
Security News
upm Launches as a Fast, Tiny Package Manager Written in TypeScript
upm uses Node.js to deliver fast npm installs in about 250 KB, with a JavaScript API and security defaults.
@vercel/queue
Advanced tools
A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
send and receive are all you needsend and receive work in any Node.js environmentnpm install @vercel/queue
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" }]
}
}
}
For local development, link your Vercel project:
npm i -g vercel
vc link
vc env pull
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 duringnpm run dev.
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:
messageIdisnullwhen the server accepts the message for deferred processing (e.g. during a server-side outage). The message will still be delivered.
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.
handleCallbackReturns (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;
handleNodeCallbackReturns (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;
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.
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.
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`,
});
The transport controls how message payloads are serialized and deserialized.
| Use Case | Transport | Memory Usage | Notes |
|---|---|---|---|
| Structured data | JsonTransport | Low | Default, JSON encoding |
| Binary data | BufferTransport | Medium | Raw bytes |
| Large payloads | StreamTransport | Very Low | No 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);
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.
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.
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:
limitandmessageIdare mutually exclusive options. The handler is never called when the queue is empty — checkresult.okinstead.
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:
| Error | Description |
|---|---|
BadRequestError | Invalid request parameters |
UnauthorizedError | Authentication failed (invalid/missing token) |
ForbiddenError | Access denied (wrong environment/project) |
DuplicateMessageError | Idempotency key already used |
ConsumerDiscoveryError | Could not reach consumer deployment |
ConsumerRegistryNotConfiguredError | Project not configured for queues |
InternalServerError | Unexpected server error |
InvalidLimitError | Batch limit outside valid range (1-10) |
MessageNotFoundError | Message doesn't exist or expired |
MessageNotAvailableError | Message exists but cannot be claimed |
MessageAlreadyProcessedError | Message already successfully processed |
MessageLockedError | Message being processed by another consumer |
MessageCorruptedError | Message data could not be parsed |
QueueEmptyError | No messages available in queue |
| Variable | Description | Default |
|---|---|---|
QUEUE_REGION | Region code for the queue client (user-defined) | - |
VERCEL_REGION | Current region (auto-set by Vercel) | - |
VERCEL_QUEUE_DEBUG | Enable debug logging (1 or true) | - |
VERCEL_DEPLOYMENT_ID | Deployment ID (auto-set by Vercel) | - |
| Limit | Value | Notes |
|---|---|---|
| Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
| Payload size | 1 GB | Smaller messages have lower latency |
| Number of topics | Unlimited | No hard limit |
| Consumer groups per message | ~4,000 | Per-message limit |
| Messages per queue | Unlimited | No hard limit |
| Parameter | Default | Min | Max | Notes |
|---|---|---|---|---|
retentionSeconds | 86,400 (24h) | 60 | 86,400 | Message TTL |
delaySeconds | 0 | 0 | ≤ retention | Cannot exceed retention |
idempotencyKey | — | — | — | Dedup window: min(retention, 24h) |
| Parameter | Default | Min | Max | Notes |
|---|---|---|---|---|
visibilityTimeoutSeconds | 300 | 30 | 3,600 | Lock duration during processing |
limit | 1 | 1 | 10 | Messages per request |
| Identifier | Pattern | Example |
|---|---|---|
| Topic name | [A-Za-z0-9_-]+ | my-queue, task_queue_v2 |
| Consumer group | [A-Za-z0-9_-]+ | worker-1, analytics_consumer |
| Message ID | Opaque string | 0-1, 3-7K9mNpQrS |
| Receipt handle | Opaque string | Used for acknowledge/visibility ops |
{
"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 nameuser-*, orders-**-events, user-*-dataQueueClientimport { 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 }),
},
);
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;
}
MIT
FAQs
A Node.js library for interacting with the Vercel Queue Service API
The npm package @vercel/queue receives a total of 1,574,551 weekly downloads. As such, @vercel/queue popularity was classified as popular.
We found that @vercel/queue demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 4 open source maintainers collaborating on the project.

Security News
upm uses Node.js to deliver fast npm installs in about 250 KB, with a JavaScript API and security defaults.

Company News
Socket is joining the OpenJS Security Stewardship Program to fund Node.js vulnerability research, maintainer remediation, and security releases.

Security News
Two compromised GitHub Actions were re-enabled with malicious tags intact, exposing thousands of downstream repositories to Mini Shai-Hulud.