A single route handler sends a confirmation email, fires a webhook at a payment provider, and starts an image pipeline before it returns a response. When the email provider stalls or the function dies mid-webhook, that work is gone, with no retry and no record of which users were affected.

Teams usually reach for a queue when something runs slow, which makes latency the test and cost the tiebreaker. Both are the wrong measure, because a queue earns its place based on what a specific piece of work can afford to lose.

This guide covers what a queue is, the mechanics that make it reliable, and how to pick the primitive that matches the failure you're trying to survive.

**Key takeaways:**

- A message queue decouples producers from consumers, so a slow or failing consumer never blocks the code that published the work.

- Durability comes from the publish, lease, acknowledge, and retry cycle, where a message survives until a consumer explicitly acknowledges it.

- Production brokers default to at-least-once delivery, which makes idempotent consumer logic a requirement rather than a refinement.

- Fluid compute removes the cost argument for queuing slow I/O, because Active CPU billing pauses while a function waits. Surviving a crash or a rollout remains the reason to add a queue.

- The right primitive matches the failure mode, with `after()` for losable side effects, Vercel Queues for durable single tasks, and Vercel Workflows for multi-step orchestration.

## [Copy link to heading](#what-is-a-message-queue)What is a message queue?

A message queue is a durable buffer that accepts messages from producers, stores them until a consumer is ready, and delivers each message to the consumers subscribed to it. The producer publishes and moves on. The consumer processes at its own pace, seconds or days later.

Neither side needs the other to be online at the same moment. Producers, brokers, and consumers communicate over the network, which is why a protocol like the Advanced Message Queuing Protocol ([AMQP 0-9-1](https://www.rabbitmq.com/tutorials/amqp-concepts)) exists at all. Separating them physically is the point of the design rather than a side effect of it.

### [Copy link to heading](#how-do-message-queues,-pub/sub,-and-event-streams-differ)How do message queues, pub/sub, and event streams differ?

Everything hinges on what happens to a message after someone reads it. A point-to-point queue hands each message to one consumer and deletes it. A publish/subscribe topic fans a message out to whoever is listening at that moment and drops it for anyone who isn't. An event stream keeps the message on a durable log and lets independent readers work through it on their own schedules.

Those models diverge across the properties that decide what you can build on top of them:

| Model | Who receives a message | State after processing | Replay | Fits |
| --- | --- | --- | --- | --- |
| Point-to-point queue | One consumer | Deleted from the queue | Not available | Task distribution where each job runs once |
| Publish/subscribe topic | Every subscriber active at publish time | Dropped for absent subscribers | Not available | Live fan-out notifications |
| Event stream | Every consumer group, each at its own position | Retained until the message expires | Available within the retention window | Several pipelines reading one set of events |

Product names don't map onto these categories. RabbitMQ routes each message to one consumer and drops it on acknowledgment, whereas Kafka keeps an append-only log that any consumer group can re-read. Their [operational tradeoffs](https://vercel.com/i/rabbitmq-vs-kafka) follow from that difference in storage. [Vercel Queues](https://vercel.com/docs/queues) belong in the third column despite the name, so a consumer group added months later can replay every non-expired message rather than waiting for new traffic to arrive.

### [Copy link to heading](#benefits-of-message-queues-for-async-workloads)Benefits of message queues for async workloads

A queue earns its place when a specific failure or load pattern carries a cost. These benefits cover most of those cases:

- **Producer and consumer decoupling:** The publishing code returns as soon as the broker accepts the message, so a downstream service that's slow, rate-limited, or down never holds a user-facing request open behind it.

- **Survival across crashes and rollouts:** The broker persists the message before acknowledging the publish, so accepted work outlives a consumer crash, a deployment rollout, or a function timeout.

- **Load leveling under burst:** A spike of inbound work lands in the log at whatever rate it arrives, so consumers drain it at a rate the downstream systems can survive.

- **Deferred and scheduled delivery:** The broker holds a message invisible until its delay expires, so reminder emails and cooldown retries need no separate scheduler.

Load leveling works differently on serverless. A fixed worker pool caps concurrency by accident, so a burst backs up in front of it whether or not anyone designed for that. Autoscaling removes that ceiling, so a payment provider replaying 4 hours of webhooks in 90 seconds sends the whole spike through to the database behind it. Publishing each webhook and acknowledging immediately puts the ceiling back.

Work nobody would miss doesn't need any of this, and a broker bought to protect it earns nothing.

## [Copy link to heading](#core-components-of-a-message-queue-system)Core components of a message queue system

Vocabulary varies between brokers, but the failure modes these mechanics address don't. Each one below exists because something in the chain between producer and consumer can fail.

### [Copy link to heading](#producers,-topics,-and-consumer-groups)Producers, topics, and consumer groups

Publishing starts with a producer, which sends a message to a topic. A consumer group subscribes to that topic and tracks its own position in it. Isolation between groups makes fan-out safe, so a group that falls behind has no effect on any other group reading the same topic. An analytics pipeline and an order-fulfillment pipeline can share one stream without sharing a fate.

On Vercel, topics belong to the project and deployment rather than to an individual service, and consumer functions have no public URL. A queue-triggered route can only be invoked by Vercel's internal queue infrastructure, which removes the authentication layer those handlers would otherwise need.

### [Copy link to heading](#the-publish,-lease,-acknowledge,-retry-cycle)The publish, lease, acknowledge, retry cycle

Queues stay reliable by refusing to delete a message until someone confirms they finished with it. Vercel Queues implement that cycle like this:

1.  A producer publishes a message to a topic.

2.  The topic makes the message available to each subscribed consumer group.

3.  A consumer receives the message under a visibility timeout lease, which hides it from other consumers in that group without deleting it.

4.  On success, the consumer acknowledges and the message is removed for that group. On failure or timeout, the lease expires and the message is redelivered.

Durability begins when the broker persists the publish. The lease and acknowledgment cycle then prevents a consumer crash from silently discarding accepted work. A message that has been read but not acknowledged is still in the log, so a consumer crashing mid-processing costs a retry rather than a lost task.

### [Copy link to heading](#delivery-guarantees-and-at-least-once-semantics)Delivery guarantees and at-least-once semantics

Every broker makes a promise about which failure is worse, losing a message or delivering it twice. At-most-once means a message can be lost but never repeated. At-least-once means it's never lost but may repeat. Exactly-once requires broker-side deduplication paired with consumers that produce the same result on a second pass.

At-least-once is the working default across production brokers, Vercel Queues included. Redelivery has ordinary causes: a consumer that misses its visibility timeout, or an infrastructure event like an availability zone failover. The guarantee therefore shifts from the broker to the handler, which is why idempotent consumer logic is the contract that makes at-least-once safe to build on.

### [Copy link to heading](#visibility-timeouts-and-redelivery)Visibility timeouts and redelivery

Every leased message carries a deadline, and a consumer that misses it is presumed failed. Too short a window makes a healthy handler produce duplicates, and too long a window leaves stuck messages invisible rather than retried.

Defaults vary more than teams expect. On AWS, Amazon Simple Queue Service (SQS) defaults its [visibility timeout](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html) to 30 seconds with a 12-hour ceiling. The Vercel Queues [JavaScript SDK](https://vercel.com/docs/queues/sdk) defaults to 300 seconds and re-extends the lease as the handler runs, whereas the underlying HTTP API defaults to 60 seconds with no auto-extension.

### [Copy link to heading](#message-retention-and-delayed-delivery)Message retention and delayed delivery

Retention determines how long an unprocessed message stays consumable. Vercel Queues retain messages for 24 hours by default, configurable per message from 60 seconds to 7 days through the `Vqs-Retention-Seconds` header after an [April 2026 increase](https://vercel.com/changelog/queues-now-supports-7-day-ttl) from the previous 24-hour ceiling.

Delayed delivery uses the same window. A message published with `Vqs-Delay-Seconds` is stored immediately but stays invisible until the delay expires, up to 7 days and never beyond the message's own time-to-live (TTL). For work that needs to wait longer than that, chaining delayed messages works, and [Vercel Workflows](https://vercel.com/docs/workflows) handle it directly with `sleep()`.

### [Copy link to heading](#retries,-backoff,-and-dead-letter-queues)Retries, backoff, and dead-letter queues

Retries handle transient failures, the ones that clear on a later attempt; [exponential backoff](https://vercel.com/i/exponential-backoff), with jitter, stops consumers from synchronizing their retries onto a recovering dependency; and a [dead-letter queue](https://vercel.com/i/dead-letter-queue-guide) (DLQ) catches permanent failures. A poison message is one that will never succeed regardless of how it is retried, usually because of a schema mismatch or a malformed payload. Most brokers, including SQS, Azure Service Bus, and RabbitMQ, route these to a DLQ after a configured number of receives.

Vercel Queues have no built-in DLQ. Failed messages retry until they expire, honoring your configured retry delay for the first 32 attempts before forcing exponential backoff. Poisoned messages are deprioritized behind messages with no delivery attempts, so a failing message can't block a consumer even at max concurrency 1.

## [Copy link to heading](#how-vercel-handles-message-queue-workloads-for-engineering-teams)How Vercel handles message queue workloads for engineering teams

Every piece of async work belongs on the cheapest primitive that can safely lose it, and moving up the ladder should require a named failure rather than a hunch. The primitives differ on exactly that axis:

| Primitive | Survives function timeout | Survives crash or rollout | Ordering | Retry control | Dead-letter handling |
| --- | --- | --- | --- | --- | --- |
| `after()` and `waitUntil` | No | No | Not applicable | None | None |
| Vercel Queues | Yes | Yes | Approximate write order | `retry` callback keyed on delivery count | Application-level |
| Vercel Workflows | Yes | Yes | Per step | 3 times by default, for a total of 4 attempts | Failures recorded in the run history |
| Vercel Cron Jobs | No | No | Runs can overlap | None | None |

Cron Jobs schedule invocations rather than guarantee delivery, so a run that doesn't land waits for the next interval. Recurring work that has to survive a missed run belongs on Queues, with cron triggering the publish.

### [Copy link to heading](#fire-off-losable-side-effects-without-blocking-the-response)Fire off losable side effects without blocking the response

Logging, analytics, and cache warming don't justify a broker. Blocking a response on them is the more common mistake.

[`after()`](https://nextjs.org/docs/app/api-reference/functions/after), stable since Next.js 15.1, schedules work to run once the response has finished streaming, and `waitUntil()` from `@vercel/functions` does the same job on earlier versions. Both inherit the function's timeout, so a function that times out cancels the pending work with no retry. That's acceptable for a dropped analytics event and unacceptable for a confirmation email.

### [Copy link to heading](#keep-durable-work-alive-across-crashes-and-rollouts)Keep durable work alive across crashes and rollouts

The work that goes missing during a deploy is the work nobody instrumented. A rollout replaces the running instance, and anything scheduled with `after()` on the old one disappears without an error anyone sees.

Vercel Queues exist for the tasks that can't tolerate this. Every message is written synchronously to [3 availability zones](https://vercel.com/docs/queues/concepts) before the publish call returns, and publishing takes one call:

```
import { send } from '@vercel/queue';

export async function POST(request: Request) {
  const body = await request.json();
  const { messageId } = await send('orders', {
    orderId: body.orderId,
    action: 'process',
  });
  return Response.json({ messageId });
}
```

Consumers run as ordinary Vercel Functions with a queue trigger in `vercel.json`, which means no worker fleet to size and no always-on process to keep alive. Queues have been in public beta on every plan [since February 2026](https://vercel.com/changelog/vercel-queues-now-in-public-beta), with `@vercel/queue` for JavaScript and `vercel-queue` for Python.

### [Copy link to heading](#orchestrate-multi-step-pipelines-without-a-state-machine)Orchestrate multi-step pipelines without a state machine

A single durable task tends to become a chain of them. Generate, then upload, then notify, then reconcile. Wiring that with raw messages means building a [state machine](https://vercel.com/i/workflow-orchestration) by hand, and the retry logic gets rewritten at every boundary.

Vercel Workflows, generally available since April 2026, are built on Queues and add durable state plus `sleep()` that releases compute while pausing. Step-level retries run 3 times by default, for a total of 4 attempts. Flora runs its media generation pipeline across more than 50 image models with no separate queue or state machine service, and Durable delivers complete AI-generated sites in under 30 seconds. Workflows have handled [over 100 million runs](/blog/a-new-programming-model-for-durable-execution) since the October 2025 beta.

### [Copy link to heading](#stop-paying-for-idle-time-rather-than-queuing-around-it)Stop paying for idle time rather than queuing around it

The standard advice says to queue anything slow so a function isn't held open billing for idle time. On Vercel, that reasoning no longer holds, and teams still following it are adding a broker to solve a billing problem that doesn't exist.

Under [Fluid compute](https://vercel.com/docs/fluid-compute), Active CPU billing pauses while a function waits on a database query, an API response, or model inference. Optimized concurrency compounds the effect, since one instance handles several invocations at once, so a function that mostly waits absorbs load without an external buffer. Durability remains the only argument, and no billing model touches it.

## [Copy link to heading](#5-practices-for-running-message-queues-in-production)5 practices for running message queues in production

These practices cover the decisions that determine whether the queue helps or becomes another thing to operate.

### [Copy link to heading](#make-consumers-idempotent-before-the-first-message-ships)Make consumers idempotent before the first message ships

Retrofitting idempotency after duplicate charges reach customers is the expensive version of this work, and the deduplication strategy costs almost nothing to add up front.

Writes can be keyed on a unique message or business identifier so a second attempt becomes a no-op, or operations can be designed to be naturally repeatable by setting values rather than incrementing them. On the publish side, an [idempotency key](https://vercel.com/i/what-is-idempotency) protects against producer-side retries after a network timeout.

### [Copy link to heading](#size-the-visibility-timeout-to-the-slowest-handler)Size the visibility timeout to the slowest handler

Teams tend to set this to the average processing time, which guarantees duplicates on the tail. The timeout should cover the slowest realistic execution, including retries against a downstream API that's having a bad day.

Automatic lease extension covers this for handlers built on the SDK. Calling the HTTP API directly means extending the lease mid-processing when a handler runs long.

### [Copy link to heading](#route-poison-messages-rather-than-retrying-to-expiry)Route poison messages rather than retrying to expiry

Retrying a permanently broken message for its full retention window converts a fast, loud failure into a slow, quiet one, and it burns delivery attempts and log volume on the way.

Reading the delivery count and acting on a threshold solves this. Acknowledging the message stops the retries, and writing the payload and error somewhere a human can find them makes triage possible.

### [Copy link to heading](#weigh-a-postgres-job-table-before-adding-a-broker)Weigh a Postgres job table before adding a broker

A job table in a database you already run handles more load than most teams assume. [SKIP LOCKED](https://www.postgresql.org/docs/current/sql-select.html) lets concurrent workers claim different rows without blocking each other, and because the lock lives inside the transaction, a failed job rolls back atomically with the work it was doing. No external broker offers that atomicity.

The cost arrives later, as sustained write volume. Vacuum pressure and write-ahead log growth show up over months rather than in the first week, and at that point the queue starts sharing a failure domain with the production database. For early-stage workloads, the tradeoff usually favors the job table.

### [Copy link to heading](#count-the-cost-of-a-broker-you-now-operate)Count the cost of a broker you now operate

Adopting a broker means operating it. Self-hosted Kafka brings partition rebalances, broker replacement, and consumer lag monitoring, a surface that grows independently of application complexity. The engineering time it consumes tends to outlast the problem it was adopted to solve.

Managed queues move that operational work onto the platform, which is the argument for choosing one over a self-hosted broker. Neither option answers whether the work needed a queue at all.

## [Copy link to heading](#start-with-the-failure-mode,-not-the-broker)Start with the failure mode, not the broker

Every piece of async work carries a price for losing it. A dropped analytics event costs nothing. A dropped confirmation email costs a support ticket, and a dropped payment webhook costs more. Naming that price first turns an architecture debate into a short decision, though where the line falls depends on what your team already runs.

Vercel provides the full ladder on one platform:

- `**after()**` **and** `**waitUntil**`**:** Post-response execution for side effects that cost nothing when a timeout cancels them.

- **Vercel Queues:** Durable event streaming with 3-zone replication, at-least-once delivery, and automatic redelivery across crashes and rollouts.

- **Vercel Workflows:** Durable steps, per-step retries, and sleeps measured in minutes or months, built on Queues with no run duration limit.

- **Fluid compute:** Active CPU billing that pauses during I/O, removing the cost pressure to queue work that only waits.

- **Queue-triggered functions:** Private consumer routes that need no auth layer of their own.

[Start a new project](https://vercel.com/new) and publish your first message with `@vercel/queue`, or browse [vercel.com/templates](https://vercel.com/templates) for a background-processing foundation to build on.

## [Copy link to heading](#frequently-asked-questions-about-message-queues)Frequently asked questions about message queues

### [Copy link to heading](#what-is-the-difference-between-a-message-queue-and-a-message-broker)What is the difference between a message queue and a message broker?

A queue is the destination that holds messages until a consumer reads them. A broker is the server software that hosts queues, routes messages to them, and enforces delivery guarantees. RabbitMQ is a broker, and a queue inside it is one of the destinations it manages.

### [Copy link to heading](#what-delivery-guarantee-do-vercel-queues-provide)What delivery guarantee do Vercel Queues provide?

Vercel Queues deliver at-least-once. A message usually arrives exactly once, but consumer timeouts and rare infrastructure events can trigger redelivery, so handlers must tolerate repeats. Producers deduplicate at publish time with the `Vqs-Idempotency-Key` header.

### [Copy link to heading](#do-vercel-queues-have-a-dead-letter-queue)Do Vercel Queues have a dead-letter queue?

No. Failed messages retry until their retention window closes. Detecting a poison message and routing it somewhere durable is application code, keyed on the delivery count each message carries.

### [Copy link to heading](#when-should-you-not-use-a-message-queue)When should you not use a message queue?

Skip the queue when losing the work costs nothing, since `after()` covers that for free. A job table in your existing database is enough when it handles the volume. Running a broker also stops making sense once it costs more than the failure you're insuring against.

### [Copy link to heading](#can-i-use-bullmq-or-a-redis-backed-queue-on-vercel)Can I use BullMQ or a Redis-backed queue on Vercel?

Not for the worker side. BullMQ workers need a persistent process, and Vercel runs queue-triggered functions rather than always-on workers. You can enqueue jobs from a function, but the worker draining that queue needs a separate always-on service.