---
title: Queues
description: Run slow work after the response. One package describes the job, and one small package per platform sends and runs it.
---

A queue is a waiting line for work. Your app puts a job on the line and
answers the user at once. Something else takes the job off the line and runs
it later. A job is one piece of work, like sending an email.

Queues are split into three packages:

- `@zap-ts/queues` says what a job looks like. It has no code, only types.
- `@zap-ts/queues-cloudflare` sends jobs and runs them on Cloudflare Queues.
- `@zap-ts/queues-vercel` sends jobs and runs them on Vercel Queues.

Your app depends on one platform package. The shape of the job stays the same
on both.

## The job

`@zap-ts/queues` has one file. It holds the `Job` type. Today there is one
kind of job:

```ts
export type MailJob = {
  type: "send-email";
  to: string;
  subject: string;
};
```

`Job` is that type. Later it will be a list of several job types. The `type`
field says which kind of job it is, so the code that runs the job knows what
to do.

Add your own job here. Both platform packages then see it, because both
import `Job` from this one place.

## Sending a job

Both platform packages export the same two calls from their `/producer` path.
A producer is the code that puts a job on the queue.

- `sendJob` puts one job on the queue.
- `sendJobs` puts several jobs on the queue.

On Cloudflare:

```ts
import { sendJob } from "@zap-ts/queues-cloudflare/producer";

const effect = sendJob(
  { type: "send-email", to: "user@example.com", subject: "Welcome!" },
  { delaySeconds: 60 },
);
```

On Vercel:

```ts
import { sendJob } from "@zap-ts/queues-vercel/producer";

const effect = sendJob({ type: "send-email", to: "user@example.com", subject: "Welcome!" });
```

Both calls need a `JobQueue` layer, which the main export of each package
builds. On Cloudflare you pass the queue binding that the Worker gives you. A
binding is the handle to a queue that Cloudflare hands your code.

```ts
import { JobQueueLive } from "@zap-ts/queues-cloudflare";
```

On Vercel you pass the name of the queue instead:

```ts
import { JobQueueLive } from "@zap-ts/queues-vercel";

const layer = JobQueueLive("zap-ts-jobs");
```

If a send fails, you get a `JobQueueError`. It carries the original error in
its `cause` field.

## Sending many jobs at once

The two platforms do not behave the same here.

On Cloudflare, `sendJobs` makes one call. Every job travels together. Each job
is wrapped in `{ body }`.

On Vercel there is no call for a group of jobs. So `sendJobs` sends them side
by side, one request each. Every job is sent, but they do not arrive as one
unit. If one send fails, that failure is reported, and the other jobs stay on
the queue.

So write every job so it can run on its own. Do not write a job that only
makes sense next to another job.

## Running a job

Both platform packages export a `/consumer` path. A consumer is the code that
takes jobs off the queue and runs them.

On Cloudflare:

```ts
import { handleJobBatch } from "@zap-ts/queues-cloudflare/consumer";
import { Effect } from "effect";

export default {
  queue: (batch) =>
    Effect.runPromise(handleJobBatch(batch, (job) => Effect.log(`sending to ${job.to}`))),
};
```

Cloudflare gives your Worker a batch. A batch is a small group of jobs.
`handleJobBatch` runs your function on each job in the batch. A job that works
is marked as done. A job that fails is written to the log and put back on the
queue, so the queue tries it again.

On Vercel, jobs do not arrive in a batch. Vercel calls a route of your own
app, once per job. So the consumer is a route, and it is built by `handleJob`:

```ts
import { createFileRoute } from "@tanstack/react-router";
import { handleJob } from "@zap-ts/queues-vercel/consumer";
import { Effect } from "effect";

export const Route = createFileRoute("/api/jobs")({
  server: {
    handlers: {
      POST: handleJob((job) => Effect.log(`sending to ${job.to}`)),
    },
  },
});
```

`handleJob` gives you the same two outcomes as `handleJobBatch`. A job that
works answers Vercel, and the job is done. A job that fails is written to the
log and the route fails, so Vercel Queues delivers the job again.

How many times it tries, and where a job goes after that, is set on the Vercel
queue itself, not in this package.

## What ships in the web app

`apps/web` runs on Cloudflare. Its `src/server.ts` file has a `queue` handler.
The handler passes the batch to `handleJobBatch` and writes one line to the
log for each job. That is a placeholder. Replace the body with the real work.

The queue is set up in `apps/web/wrangler.jsonc`. It is named `zap-ts-jobs`.
The binding is `QUEUE`. A batch holds up to 10 jobs, or waits 5 seconds for
them. A job is tried up to 3 times. After that it goes to `zap-ts-jobs-dlq`. A
dead letter queue holds the jobs that never worked, so you can look at them
later.

## Related

- [Mail](/packages/mail) — the work the one job type does
- [Web](/apps/web) — the app that runs the jobs
- [Going to production](/guides/going-to-production) — creating the real queue
