New:Microsoft Teams Notifications Are Now Available in Socket.Learn more →
Get Started

@vercel/queue

Package Overview
Dependencies
Maintainers
5
Versions
58
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@vercel/queue

A Node.js library for interacting with the Vercel Queue Service API

npmnpm
Version
0.0.0-alpha.40
Version published
Weekly downloads
1.7M
59.86%
Maintainers
5
Weekly downloads
 
Created
Source

Vercel Queues

A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.

Features

  • Simple API: send and receive are all you need
  • Automatic Triggering on Vercel: Vercel invokes your route handlers when messages are ready
  • Works Anywhere: send and receive work in any Node.js environment
  • Type Safety: Full TypeScript generics support
  • Customizable Serialization: Built-in JSON, Buffer, and Stream transports
  • Local Dev Mode: Messages sent locally trigger your handlers automatically

Installation

npm install @vercel/queue

Quick Start

Set up your region via environment variables. If your framework supports .env files (Next.js, Vite, Nuxt, etc.):

# .env.production (on Vercel, inherits the platform's region)
QUEUE_REGION=${VERCEL_REGION}

# .env.development (fixed region for local dev — iad1 is recommended)
QUEUE_REGION=iad1

Otherwise, set QUEUE_REGION in your environment directly (e.g. via your hosting provider's dashboard or a dotenv setup).

Create a shared queue client:

// lib/queue.ts
import { QueueClient } from "@vercel/queue";

const queue = new QueueClient({ region: process.env.QUEUE_REGION! });
export const { send, receive, handleCallback, handleNodeCallback } = queue;

Send a message anywhere in your app:

import { send } from "@/lib/queue";

await send("my-topic", { message: "Hello world" });

Handle incoming messages with a route handler:

// app/api/queue/my-topic/route.ts
import { handleCallback } from "@/lib/queue";

export const POST = handleCallback(async (message, metadata) => {
  console.log("Processing:", message);
});

Configure your vercel.json:

{
  "functions": {
    "app/api/queue/my-topic/route.ts": {
      "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
    }
  }
}

Project Setup

For local development, link your Vercel project:

npm i -g vercel
vc link
vc env pull

Local Development

Queues just work locally. When you send() messages in development mode, the library sends them to the real Vercel Queue Service, reads your vercel.json configuration, discovers your queue handlers, and triggers them automatically via local HTTP requests. This means your local dev environment behaves identically to production — no surprising behavior differences.

Note: Local dev mode is enabled when NODE_ENV=development. Most frameworks (Next.js, etc.) set this automatically during npm run dev.

Publishing Messages

import { QueueClient } from "@vercel/queue";

const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });

// Simple send
await send("my-topic", { message: "Hello world" });

// With options
await send(
  "my-topic",
  { message: "Hello world" },
  {
    idempotencyKey: "unique-key", // Prevent duplicate messages
    retentionSeconds: 3600, // 1 hour TTL (default: 24h)
    delaySeconds: 60, // Delay delivery by 1 minute
  },
);

Example usage in an API route:

// app/api/send-message/route.ts
import { send } from "@/lib/queue";

export async function POST(request: Request) {
  const body = await request.json();
  const { messageId } = await send("my-topic", { message: body.message });
  return Response.json({ messageId });
}

Note: messageId is null when the server accepts the message for deferred processing (e.g. during a server-side outage). The message will still be delivered.

Consuming Messages

On Vercel

On Vercel, messages are consumed using API route handlers that Vercel automatically invokes when messages are available. Use handleCallback or handleNodeCallback to create these route handlers.

Web API — handleCallback

Returns (Request) => Promise<Response>. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).

Next.js App Router:

// app/api/queue/my-topic/route.ts
import { handleCallback } from "@/lib/queue";

export const POST = handleCallback(async (message, metadata) => {
  // metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
  await processMessage(message);
  // Throwing an error will automatically retry the message
});

Hono:

import { Hono } from "hono";
import { handleCallback } from "@/lib/queue";

const app = new Hono();
app.post(
  "/api/queue",
  handleCallback(async (message, metadata) => {
    await processMessage(message);
  }),
);
export default app;

Connect-style — handleNodeCallback

Returns (req, res) => Promise<void>. For frameworks that export Connect-style handlers (Express, Next.js Pages Router, etc.).

Next.js Pages Router:

// pages/api/queue/my-topic.ts
import { handleNodeCallback } from "@/lib/queue";

export default handleNodeCallback(async (message, metadata) => {
  await processMessage(message);
});

Express:

import express from "express";
import { handleNodeCallback } from "@/lib/queue";

const app = express();
app.use(express.json());
app.post(
  "/api/queue/my-topic",
  handleNodeCallback(async (message, metadata) => {
    await processMessage(message);
  }),
);
export default app;

2. Configure vercel.json

Tell Vercel which routes handle which topics:

{
  "functions": {
    "app/api/queue/my-topic/route.ts": {
      "experimentalTriggers": [
        {
          "type": "queue/v2beta",
          "topic": "my-topic",
          "retryAfterSeconds": 60,
          "initialDelaySeconds": 0
        }
      ]
    },
    "app/api/queue/orders/fulfillment/route.ts": {
      "experimentalTriggers": [
        { "type": "queue/v2beta", "topic": "order-events" }
      ]
    },
    "app/api/queue/orders/analytics/route.ts": {
      "experimentalTriggers": [
        {
          "type": "queue/v2beta",
          "topic": "order-events",
          "retryAfterSeconds": 300
        }
      ]
    }
  }
}

Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.

3. Retry and Backoff

When a handler throws, the message is not acknowledged and becomes available for redelivery after the retryAfterSeconds interval configured in vercel.json. Retries continue until the handler succeeds or the message expires (default: 24 hours).

For finer control over retry timing, pass a retry option:

export const POST = handleCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    retry: (error, metadata) => {
      if (error instanceof RateLimitError) return { afterSeconds: 60 };
      // Return undefined to let the error propagate normally
    },
  },
);

When retry returns { afterSeconds: N }, the message is rescheduled for redelivery after N seconds. Return { acknowledge: true } to acknowledge the message so it is never retried. When it returns undefined, the error propagates normally and the message is retried at the default interval.

Exponential backoff uses metadata.deliveryCount (starts at 1, increments each delivery):

export const POST = handleCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    retry: (error, metadata) => {
      // 5s → 10s → 20s → 40s → ... capped at 5 min
      const delay = Math.min(300, 2 ** metadata.deliveryCount * 5);
      return { afterSeconds: delay };
    },
  },
);

Conditional retry — only retry transient errors:

export const POST = handleCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    retry: (error, metadata) => {
      if (error instanceof RateLimitError) return { afterSeconds: 60 };
      if (error instanceof TemporaryError) return { afterSeconds: 30 };
      // Permanent errors: return undefined → retried at the default interval
    },
  },
);

Acknowledging poison messages — stop retrying messages that can never succeed:

export const POST = handleCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    retry: (error, metadata) => {
      if (error instanceof ValidationError) return { acknowledge: true };
      if (metadata.deliveryCount > 5) return { acknowledge: true };
      return { afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5) };
    },
  },
);

The retry option is available on handleCallback, handleNodeCallback, and receive.

Custom Client Configuration

All configuration lives on the QueueClient:

import { QueueClient, BufferTransport } from "@vercel/queue";

const queue = new QueueClient({
  region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
  token: "my-token", // Auth token (default: OIDC auto-detection)
  transport: new BufferTransport(), // Serialization (default: JsonTransport)
  headers: { "X-Custom": "header" }, // Custom headers on all requests
  deploymentId: null, // null = unpinned, omit = auto from env, or explicit string
});

// Use directly
await queue.send("my-topic", myBuffer);

// Or destructure
export const { send, receive, handleCallback, handleNodeCallback } = queue;

The client sends requests to https://${region}.vercel-queue.com. When handleCallback receives a message, it reads the ce-vqsregion header and routes follow-up API calls to the correct regional endpoint.

To customize the URL scheme, provide a resolveBaseUrl:

const queue = new QueueClient({
  region: process.env.QUEUE_REGION!,
  resolveBaseUrl: (region) => `https://${region}.my-proxy.example`,
});

Transports

The transport controls how message payloads are serialized and deserialized.

Use CaseTransportMemory UsageNotes
Structured dataJsonTransportLowDefault, JSON encoding
Binary dataBufferTransportMediumRaw bytes
Large payloadsStreamTransportVery LowNo buffering, streaming
import {
  QueueClient,
  JsonTransport,
  BufferTransport,
  StreamTransport,
} from "@vercel/queue";

// JSON with custom serialization
const queue = new QueueClient({
  region: process.env.QUEUE_REGION!,
  transport: new JsonTransport({
    replacer: (key, value) => (key === "password" ? undefined : value),
    reviver: (key, value) => (key === "date" ? new Date(value) : value),
  }),
});

// Binary data
const binQueue = new QueueClient({
  region: process.env.QUEUE_REGION!,
  transport: new BufferTransport(),
});
await binQueue.send("binary-topic", myBuffer);

// Streaming for large payloads
const streamQueue = new QueueClient({
  region: process.env.QUEUE_REGION!,
  transport: new StreamTransport(),
});
await streamQueue.send("large-file", myReadableStream);

Manual Receive

Use receive to pull and process messages directly. This is an advanced alternative to handleCallback that works in any Node.js environment, both on and off Vercel.

Region considerations

Messages can only be received from the region they were sent to. When using receive, use a fixed region (e.g. "iad1") for both sending and receiving — do not use VERCEL_REGION (or QUEUE_REGION=${VERCEL_REGION}), because Vercel may route requests to different regions due to failover or load balancing, distributing your messages across regions unpredictably.

# .env.production — fixed region for manual receive workflows
QUEUE_REGION=iad1

# .env.development
QUEUE_REGION=iad1

A single region is still highly available — Vercel deploys across 3+ availability zones within each region. If you need multi-region availability, you are responsible for designing your own HA strategy (e.g. sending to multiple regions and receiving from each).

For most use cases on Vercel, handleCallback is the recommended approach — the platform handles region routing automatically and the SDK routes follow-up calls to the correct region via the ce-vqsregion header.

Usage

import { QueueClient } from "@vercel/queue";

const { receive } = new QueueClient({ region: "iad1" });

// Process next available message
const result = await receive(
  "my-topic",
  "my-group",
  async (message, metadata) => {
    console.log("Processing:", message);
  },
);
if (!result.ok) {
  console.log("Queue was empty:", result.reason);
}

// Batch processing: up to 10 messages in one request
await receive("my-topic", "my-group", handler, { limit: 10 });

// Process a specific message by ID
await receive("my-topic", "my-group", handler, { messageId: "msg-123" });

Note: limit and messageId are mutually exclusive options. The handler is never called when the queue is empty — check result.ok instead.

Error Handling

import {
  BadRequestError,
  DuplicateMessageError,
  ForbiddenError,
  InternalServerError,
  UnauthorizedError,
} from "@vercel/queue";
import { send } from "@/lib/queue";

try {
  await send("my-topic", payload);
} catch (error) {
  if (error instanceof UnauthorizedError) {
    console.log("Invalid token - refresh authentication");
  } else if (error instanceof ForbiddenError) {
    console.log("Environment mismatch - check configuration");
  } else if (error instanceof BadRequestError) {
    console.log("Invalid parameters:", error.message);
  } else if (error instanceof DuplicateMessageError) {
    console.log("Duplicate message:", error.idempotencyKey);
  } else if (error instanceof InternalServerError) {
    console.log("Server error - retry with backoff");
  }
}

All error types:

ErrorDescription
BadRequestErrorInvalid request parameters
UnauthorizedErrorAuthentication failed (invalid/missing token)
ForbiddenErrorAccess denied (wrong environment/project)
DuplicateMessageErrorIdempotency key already used
ConsumerDiscoveryErrorCould not reach consumer deployment
ConsumerRegistryNotConfiguredErrorProject not configured for queues
InternalServerErrorUnexpected server error
InvalidLimitErrorBatch limit outside valid range (1-10)
MessageNotFoundErrorMessage doesn't exist or expired
MessageNotAvailableErrorMessage exists but cannot be claimed
MessageAlreadyProcessedErrorMessage already successfully processed
MessageLockedErrorMessage being processed by another consumer
MessageCorruptedErrorMessage data could not be parsed
QueueEmptyErrorNo messages available in queue

Environment Variables

VariableDescriptionDefault
QUEUE_REGIONRegion code for the queue client (user-defined)-
VERCEL_REGIONCurrent region (auto-set by Vercel)-
VERCEL_QUEUE_DEBUGEnable debug logging (1 or true)-
VERCEL_DEPLOYMENT_IDDeployment ID (auto-set by Vercel)-

Service Limits & Constraints

Throughput & Storage

LimitValueNotes
Message throughput10,000+ msg/sec/topicScales horizontally
Payload size1 GBSmaller messages have lower latency
Number of topicsUnlimitedNo hard limit
Consumer groups per message~4,000Per-message limit
Messages per queueUnlimitedNo hard limit

Parameter Constraints

Publishing Messages

ParameterDefaultMinMaxNotes
retentionSeconds86,400 (24h)6086,400Message TTL
delaySeconds00≤ retentionCannot exceed retention
idempotencyKey———Dedup window: min(retention, 24h)

Receiving Messages

ParameterDefaultMinMaxNotes
visibilityTimeoutSeconds300303,600Lock duration during processing
limit1110Messages per request

Identifier Formats

IdentifierPatternExample
Topic name[A-Za-z0-9_-]+my-queue, task_queue_v2
Consumer group[A-Za-z0-9_-]+worker-1, analytics_consumer
Message IDOpaque string0-1, 3-7K9mNpQrS
Receipt handleOpaque stringUsed for acknowledge/visibility ops

Wildcard Topics

{
  "functions": {
    "app/api/queue/route.ts": {
      "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
    }
  }
}
  • * may only appear once in the pattern
  • * must be at the end of the topic name
  • Valid: user-*, orders-*
  • Invalid: *-events, user-*-data

API Reference

QueueClient

import { QueueClient } from "@vercel/queue";

const queue = new QueueClient({
  region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
  resolveBaseUrl: (r) => `https://${r}.vercel-queue.com`, // Default resolver
  token: "my-token", // Auto-fetched via OIDC if omitted
  headers: { "X-Custom": "value" },
  transport: new JsonTransport(), // Default: JsonTransport
  deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
});

// Methods (arrow functions — safe to destructure)
const { send, receive, handleCallback, handleNodeCallback } = queue;

send(topicName, payload, options?)

Returns { messageId: string | null }. messageId is null when the server accepted the message for deferred processing (e.g. during a server-side outage).

const { messageId } = await send("my-topic", payload, {
  idempotencyKey: "unique-key", // Dedup window: min(retention, 24h)
  retentionSeconds: 3600, // Message TTL (default: 86400)
  delaySeconds: 60, // Delay before visible (default: 0)
  headers: { "X-Custom": "val" }, // Custom headers
});

receive(topicName, consumerGroup, handler, options?)

Returns a discriminated result: { ok: true } on success, or { ok: false, reason } when no message was processed. The handler is never called when the queue is empty.

For receive-by-id, operational errors are returned instead of thrown:

const result = await receive("my-topic", "my-group", handler, {
  messageId: "msg-123",
});
if (!result.ok) {
  // result.reason is "not_found" | "not_available" | "already_processed"
  console.log(result.reason, result.messageId);
}
// Batch mode
const result = await receive("my-topic", "my-group", handler, {
  limit: 10, // Max messages (default: 1, max: 10)
  visibilityTimeoutSeconds: 60, // Lock duration (default: 300)
});

handleCallback(handler, options?)

Vercel only. Returns (request: Request) => Promise<Response> — for frameworks that export Web API route handlers.

export const POST = handleCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    visibilityTimeoutSeconds: 300, // Lock duration (default: 300)
    retry: (error, metadata) => {
      // Optional: return { afterSeconds: N } to reschedule, { acknowledge: true } to ack, or undefined to propagate
    },
  },
);

handleNodeCallback(handler, options?)

Vercel only. Returns (req, res) => Promise<void> — for frameworks that export Connect-style handlers.

// pages/api/queue/my-topic.ts
export default handleNodeCallback(
  async (message, metadata) => {
    await processMessage(message);
  },
  {
    retry: (error, metadata) => ({ afterSeconds: 60 }),
  },
);

Handler Signature

type MessageHandler<T> = (
  message: T,
  metadata: MessageMetadata,
) => Promise<void> | void;

interface MessageMetadata {
  messageId: string;
  deliveryCount: number;
  createdAt: Date;
  expiresAt?: Date;
  topicName: string;
  consumerGroup: string;
  region: string;
}

License

MIT

Keywords

vercel

FAQs

Package last updated on 25 Feb 2026

Related posts