Skip to main content

Run background work with Queues

Publish messages to durable topics with @vercel/queue and process them in your functions, with retries, delays and at-least-once delivery.

Queues gives your functions durable topics: you publish a message, and every consumer group on the topic receives it at least once, with retries until it's processed or expires. Use it to move slow work out of a request, absorb spikes, or fan one event out to several pipelines.

Easel implements the same contract as Vercel Queues, so code written with @vercel/queue runs unchanged. There's nothing to set up: topics exist on first publish, and your functions are authenticated automatically. Queues is in beta.

Publish and consume

Install the SDK:

npm i @vercel/queue

Publish from any function:

app/api/orders/route.ts
import { send } from "@vercel/queue";

export async function POST(request: Request) {
  const order = await request.json();
  const { messageId } = await send("orders", { orderId: order.id });
  return Response.json({ messageId });
}

Consume with a route handler wrapped in handleCallback. The message is acknowledged when your handler returns, and retried if it throws:

app/api/queues/fulfill-order/route.ts
import { handleCallback } from "@vercel/queue";

export const POST = handleCallback(async (order, metadata) => {
  await fulfillOrder(order, metadata.messageId);
});

Then make that route a consumer of the topic in vercel.json:

vercel.json
{
  "functions": {
    "app/api/queues/fulfill-order/route.ts": {
      "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "orders" }]
    }
  }
}

Easel invokes the route for each message. Consumer routes have no public URL: a request to one from outside returns 404, so it needs no auth of its own.

For a Pages Router or Express handler, use handleNodeCallback on a QueueClient. To pull messages on your own schedule instead, use PollingQueueClient#receive with a consumer group name of your choice.

How delivery works

  • Every group gets every message. Each route with a trigger is its own consumer group, so two routes on one topic each get a copy. A new poll group starts with the oldest retained message.
  • At least once. A message can arrive twice, for example when a handler runs past its lease, so make handlers idempotent. Use metadata.messageId to deduplicate.
  • Retries. A delivery fails when the handler throws, crashes or times out. The message is retried after the trigger's retryAfterSeconds (60 seconds by default), or after what the SDK's retry callback returns. Set maxDeliveries on the trigger to stop after that many attempts.
  • Approximate order. Messages arrive roughly in publish order, and new messages go before retries, so a failing message never blocks the rest.
  • Pinned to the deployment. A deployment's consumers only receive the messages that deployment published, so a new message format never reaches old code. After you promote or roll back, the old deployment keeps draining its own messages until they're processed or expire. Deleting a deployment stops its deliveries.

Trigger options

OptionDefaultWhat it does
topicTopic to consume. * matches any characters (order-*)
retryAfterSeconds60Wait before retrying a failed delivery
initialDelaySeconds0Wait after publish before the first delivery
maxDeliveriesUnlimitedDeliveries before a message is dropped for this consumer
maxConcurrencyUnlimitedMessages this consumer processes at once

Publish options

await send("orders", order, {
  delaySeconds: 60, // hidden from consumers until then
  retentionSeconds: 3600, // expires after an hour (default 24 hours)
  idempotencyKey: `order-${order.id}`, // a repeat publish is dropped
});

A repeat publish with the same idempotency key, within the message's retention (up to 24 hours), succeeds but is never delivered. experimental_sendBatch publishes up to 100 messages in one request.

Monitor queues

Open your project's Observability tab and select the Queues card. You'll see messages published, received and processed over the time range, and for each topic what's waiting now and how long the oldest message has waited. A poll consumer group that hasn't received in over an hour is idle: its backlog isn't counted, so one abandoned reader doesn't make a topic look stuck. Open a topic for each consumer group's:

  • Max message age: how long messages waited before a consumer got them. A climbing line means the group is falling behind.
  • Retry depth: the highest delivery count any message reached. A line that keeps rising points to a message your consumer can't process.
  • Waiting, in flight, dropped and processed counts, and the last error your consumer returned.

Messages stay with the deployment that published them, so an older deployment keeps delivering, and retrying, its own messages after you deploy again. The page tells you when an older production deployment still has messages waiting.

Workflows built with the Workflow SDK run on Queues, and their runs have their own page: see Workflows.

Limits

Messages can be up to 4 MiB and are kept for up to 7 days. See Queues limits for the rest. Queues run in iad1, the same region as your functions. Python subscribers and Vercel Services aren't supported yet.

Pricing

Queues bill per API operation, in 4 KiB chunks of each message: publishing a 12 KiB message is three operations. A publish with an idempotency key, and a delivery to a consumer with maxConcurrency, count double. Acknowledging a message or changing its lease is one operation. See Pricing for the rate and what each plan includes. The functions that process messages bill like any other function.