@vercel/queue 0.0.0-alpha.8 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,20 +1,15 @@
1
1
  # Vercel Queues
2
2
 
3
- A TypeScript client library for interacting with the Vercel Queue Service API
4
- with customizable serialization/deserialization (transport) support, including
5
- **streaming support** for memory-efficient processing of large payloads.
3
+ A TypeScript client library for interacting with the Vercel Queue Service API, designed for seamless integration with Vercel deployments.
6
4
 
7
5
  ## Features
8
6
 
9
- - **Generic Payload Support**: Send and receive any type of data with type
10
- safety
11
- - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream)
12
- or create your own
13
- - **Streaming Support**: Handle large payloads without loading them entirely
14
- into memory
15
- - **Pub/Sub Pattern**: Topic-based messaging with consumer groups
16
- - **Type Safety**: Full TypeScript support with generic types
17
- - **Automatic Retries**: Built-in visibility timeout management
7
+ - **Simple API**: `send` and `receive` are all you need
8
+ - **Automatic Triggering on Vercel**: Vercel invokes your route handlers when messages are ready
9
+ - **Works Anywhere**: `send` and `receive` work in any Node.js environment
10
+ - **Type Safety**: Full TypeScript generics support
11
+ - **Customizable Serialization**: Built-in JSON, Buffer, and Stream transports
12
+ - **Local Dev Mode**: Messages sent locally trigger your handlers automatically
18
13
 
19
14
  ## Installation
20
15
 
@@ -24,245 +19,211 @@ npm install @vercel/queue
24
19
 
25
20
  ## Quick Start
26
21
 
27
- For local development, you'll need to pull your Vercel environment variables
28
- (including the OIDC token):
22
+ Set up your region via environment variables. If your framework supports `.env` files (Next.js, Vite, Nuxt, etc.):
29
23
 
30
24
  ```bash
31
- # Install Vercel CLI if you haven't already
32
- npm i -g vercel
25
+ # .env.production (on Vercel, inherits the platform's region)
26
+ QUEUE_REGION=${VERCEL_REGION}
33
27
 
34
- # Pull environment variables from your Vercel project
35
- vc env pull
28
+ # .env.development (fixed region for local dev — iad1 is recommended)
29
+ QUEUE_REGION=iad1
36
30
  ```
37
31
 
38
- Publishing and consuming messages on a queue
32
+ Otherwise, set `QUEUE_REGION` in your environment directly (e.g. via your hosting provider's dashboard or a `dotenv` setup).
33
+
34
+ Create a shared queue client:
39
35
 
40
36
  ```typescript
41
- // index.ts
42
- import { send, receive } from "@vercel/queue";
43
-
44
- type Message = {
45
- message: string;
46
- timestamp: number;
47
- };
48
-
49
- // Send a message to a topic
50
- await send<Message>("my-topic", {
51
- message: "Hello, World!",
52
- timestamp: Date.now(),
53
- });
37
+ // lib/queue.ts
38
+ import { QueueClient } from "@vercel/queue";
54
39
 
55
- // Consume a single message off the queue
56
- // (Often wrapped in a loop to keep polling messages off the queue)
57
- await receive<Message>("my-topic", "my-consumer-group", (message, metadata) => {
58
- console.log("Received:", message.message);
59
- console.log("Timestamp:", new Date(message.timestamp));
60
- console.log("Message Metadata", metadata);
61
- // => { messageId, deliveryCount, timestamp }
62
- });
40
+ const queue = new QueueClient({ region: process.env.QUEUE_REGION! });
41
+ export const { send, receive, handleCallback, handleNodeCallback } = queue;
63
42
  ```
64
43
 
65
- ## Usage with Vercel
44
+ Send a message anywhere in your app:
66
45
 
67
- When deploying on Vercel, rather than having a persistent server subscribed to a
68
- queue, Vercel can automatically trigger your API routes when messages are ready for
69
- consumption based on your vercel.json configuration.
46
+ ```typescript
47
+ import { send } from "@/lib/queue";
70
48
 
71
- To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
72
- existing app or create one using
73
- [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
49
+ await send("my-topic", { message: "Hello world" });
50
+ ```
74
51
 
75
- ### TypeScript Configuration
52
+ Handle incoming messages with a route handler:
76
53
 
77
- Update your `tsconfig.json` to use `"bundler"` module resolution for proper
78
- package export resolution:
54
+ ```typescript
55
+ // app/api/queue/my-topic/route.ts
56
+ import { handleCallback } from "@/lib/queue";
57
+
58
+ export const POST = handleCallback(async (message, metadata) => {
59
+ console.log("Processing:", message);
60
+ });
61
+ ```
62
+
63
+ Configure your `vercel.json`:
79
64
 
80
65
  ```json
81
66
  {
82
- "compilerOptions": {
83
- "moduleResolution": "bundler"
84
- // ... other options
67
+ "functions": {
68
+ "app/api/queue/my-topic/route.ts": {
69
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
70
+ }
85
71
  }
86
72
  }
87
73
  ```
88
74
 
89
- ### Publishing messages to a queue
75
+ ### Project Setup
90
76
 
91
- Create a new server function to publish messages
77
+ For local development, link your Vercel project:
78
+
79
+ ```bash
80
+ npm i -g vercel
81
+ vc link
82
+ vc env pull
83
+ ```
84
+
85
+ ## Local Development
86
+
87
+ **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.
88
+
89
+ > **Note:** Local dev mode is enabled when `NODE_ENV=development`. Most frameworks (Next.js, etc.) set this automatically during `npm run dev`.
90
+
91
+ ## Publishing Messages
92
92
 
93
93
  ```typescript
94
- // app/actions.ts
95
- "use server";
94
+ import { QueueClient } from "@vercel/queue";
96
95
 
97
- import { send } from "@vercel/queue";
96
+ const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
98
97
 
99
- export async function publishTestMessage(message: string) {
100
- const { messageId } = await send("my-topic", {
101
- message,
102
- timestamp: Date.now(),
103
- });
98
+ // Simple send
99
+ await send("my-topic", { message: "Hello world" });
104
100
 
105
- console.log(`Published message ${messageId}`);
106
- }
101
+ // With options
102
+ await send(
103
+ "my-topic",
104
+ { message: "Hello world" },
105
+ {
106
+ idempotencyKey: "unique-key", // Prevent duplicate messages
107
+ retentionSeconds: 3600, // 1 hour TTL (default: 24h)
108
+ delaySeconds: 60, // Delay delivery by 1 minute
109
+ },
110
+ );
107
111
  ```
108
112
 
109
- Now wire up the server function to your app
113
+ Example usage in an API route:
110
114
 
111
- ```jsx
112
- // app/some/page.tsx
113
- "use client";
114
- import { publishTestMessage } from "./actions";
115
+ ```typescript
116
+ // app/api/send-message/route.ts
117
+ import { send } from "@/lib/queue";
115
118
 
116
- export default function Page() {
117
- return (
118
- // ...
119
- <button onClick={() => publishTestMessage("Hello world")}>
120
- Publish Test Message
121
- </button>
122
- );
119
+ export async function POST(request: Request) {
120
+ const body = await request.json();
121
+ const { messageId } = await send("my-topic", { message: body.message });
122
+ return Response.json({ messageId });
123
123
  }
124
124
  ```
125
125
 
126
- ### Consuming the queue
126
+ > **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.
127
127
 
128
- Messages are consumed using consumer groups, which provide load balancing and parallel processing capabilities.
128
+ ## Consuming Messages
129
129
 
130
- ## Usage with Vercel
130
+ ### On Vercel
131
131
 
132
- To consume queue messages in a Vercel deployment, you need to create (Next.js) API routes and configure them in your `vercel.json` file.
132
+ 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.
133
133
 
134
- ### 1. Create API Routes
134
+ #### Web API — `handleCallback`
135
135
 
136
- Create API routes to handle incoming queue messages using the `handleCallback` helper:
136
+ Returns `(Request) => Promise<Response>`. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).
137
137
 
138
- ```typescript
139
- // app/api/queue/handle/route.ts
140
- import { handleCallback } from "@vercel/queue";
141
-
142
- // Option 1: Single topic with multiple consumer groups
143
- export const POST = handleCallback({
144
- "my-topic": {
145
- "consumer-group-1": async (message, metadata) => {
146
- console.log(`Consumer group 1 processing:`, message, metadata);
147
- // Handle consumer group 1 logic
148
- await processGroup1(message);
149
- },
150
- "consumer-group-2": async (message, metadata) => {
151
- console.log(`Consumer group 2 processing:`, message, metadata);
152
- // Handle consumer group 2 logic
153
- await processGroup2(message);
154
- },
155
- },
156
- });
157
-
158
- async function processGroup1(message: any) {
159
- // Consumer group 1 specific logic
160
- }
161
-
162
- async function processGroup2(message: any) {
163
- // Consumer group 2 specific logic
164
- }
165
- ```
138
+ **Next.js App Router:**
166
139
 
167
140
  ```typescript
168
- // Alternative: Multiple topics in one handler
169
- export const POST = handleCallback({
170
- "user-events": {
171
- welcome: async (message, metadata) => {
172
- console.log(`New user event:`, message, metadata);
173
- await sendWelcomeEmail(message.email);
174
- },
175
- },
176
- "order-events": {
177
- fulfillment: async (order, metadata) => {
178
- console.log(`Processing order:`, order, metadata);
179
- await fulfillOrder(order);
180
- },
181
- analytics: async (order, metadata) => {
182
- console.log(`Tracking order:`, order, metadata);
183
- await trackOrder(order);
184
- },
185
- },
141
+ // app/api/queue/my-topic/route.ts
142
+ import { handleCallback } from "@/lib/queue";
143
+
144
+ export const POST = handleCallback(async (message, metadata) => {
145
+ // metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
146
+ await processMessage(message);
147
+ // Throwing an error will automatically retry the message
186
148
  });
187
149
  ```
188
150
 
189
- ### 2. Configure vercel.json
190
-
191
- Create a `vercel.json` file in your project root to declare which topics and consumer groups each API route handles:
151
+ **Hono:**
192
152
 
193
- ```json
194
- {
195
- "functions": {
196
- "app/api/queue/handle/route.ts": {
197
- "experimentalTriggers": [
198
- {
199
- "type": "queue/v1beta",
200
- "topic": "my-topic",
201
- "consumer": "consumer-group-1"
202
- },
203
- {
204
- "type": "queue/v1beta",
205
- "topic": "my-topic",
206
- "consumer": "consumer-group-2"
207
- }
208
- ]
209
- }
210
- }
211
- }
153
+ ```typescript
154
+ import { Hono } from "hono";
155
+ import { handleCallback } from "@/lib/queue";
156
+
157
+ const app = new Hono();
158
+ app.post(
159
+ "/api/queue",
160
+ handleCallback(async (message, metadata) => {
161
+ await processMessage(message);
162
+ }),
163
+ );
164
+ export default app;
212
165
  ```
213
166
 
214
- ### 3. Multiple API Routes
167
+ #### Connect-style `handleNodeCallback`
215
168
 
216
- You can also create separate API routes for different topics:
169
+ Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Express, Next.js Pages Router, etc.).
170
+
171
+ **Next.js Pages Router:**
217
172
 
218
173
  ```typescript
219
- // app/api/queue/users/route.ts - Handle user events
220
- import { handleCallback } from "@vercel/queue";
221
-
222
- export const POST = handleCallback({
223
- "user-events": {
224
- processors: async (user, metadata) => {
225
- console.log(`Processing user event:`, user, metadata);
226
- await sendWelcomeEmail(user.email);
227
- },
228
- },
174
+ // pages/api/queue/my-topic.ts
175
+ import { handleNodeCallback } from "@/lib/queue";
176
+
177
+ export default handleNodeCallback(async (message, metadata) => {
178
+ await processMessage(message);
229
179
  });
230
180
  ```
231
181
 
182
+ **Express:**
183
+
232
184
  ```typescript
233
- // app/api/queue/orders/route.ts - Handle order events
234
- import { handleCallback } from "@vercel/queue";
235
-
236
- export const POST = handleCallback({
237
- "order-events": {
238
- fulfillment: async (order, metadata) => {
239
- console.log(`Processing order:`, order, metadata);
240
- await fulfillOrder(order);
241
- },
242
- },
243
- });
185
+ import express from "express";
186
+ import { handleNodeCallback } from "@/lib/queue";
187
+
188
+ const app = express();
189
+ app.use(express.json());
190
+ app.post(
191
+ "/api/queue/my-topic",
192
+ handleNodeCallback(async (message, metadata) => {
193
+ await processMessage(message);
194
+ }),
195
+ );
196
+ export default app;
244
197
  ```
245
198
 
246
- With corresponding `vercel.json`:
199
+ ### 2. Configure vercel.json
200
+
201
+ Tell Vercel which routes handle which topics:
247
202
 
248
203
  ```json
249
204
  {
250
205
  "functions": {
251
- "app/api/queue/users/route.ts": {
206
+ "app/api/queue/my-topic/route.ts": {
252
207
  "experimentalTriggers": [
253
208
  {
254
- "type": "queue/v1beta",
255
- "topic": "user-events",
256
- "consumer": "processors"
209
+ "type": "queue/v2beta",
210
+ "topic": "my-topic",
211
+ "retryAfterSeconds": 60,
212
+ "initialDelaySeconds": 0
257
213
  }
258
214
  ]
259
215
  },
260
- "app/api/queue/orders/route.ts": {
216
+ "app/api/queue/orders/fulfillment/route.ts": {
217
+ "experimentalTriggers": [
218
+ { "type": "queue/v2beta", "topic": "order-events" }
219
+ ]
220
+ },
221
+ "app/api/queue/orders/analytics/route.ts": {
261
222
  "experimentalTriggers": [
262
223
  {
263
- "type": "queue/v1beta",
224
+ "type": "queue/v2beta",
264
225
  "topic": "order-events",
265
- "consumer": "fulfillment"
226
+ "retryAfterSeconds": 300
266
227
  }
267
228
  ]
268
229
  }
@@ -270,522 +231,425 @@ With corresponding `vercel.json`:
270
231
  }
271
232
  ```
272
233
 
273
- ### Key Points
274
-
275
- - **Automatic Triggering**: Vercel automatically triggers your API routes when messages are available for the configured topic/consumer combinations
276
- - **Message Processing**: Your API routes receive the message ID and other metadata via headers, then use the queue client to process the specific message
277
- - **Configuration Required**: The `vercel.json` file is essential - it tells Vercel which topics and consumers each route should handle
278
- - **No Polling**: Unlike traditional queue consumers, you don't need to poll for messages - Vercel handles the triggering automatically
234
+ Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
279
235
 
280
- ## Key Features
236
+ ### 3. Retry and Backoff
281
237
 
282
- ### Consumer Groups
238
+ 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).
283
239
 
284
- Multiple consumers can process messages from the same topic in parallel:
240
+ For finer control over retry timing, pass a `retry` option:
285
241
 
286
242
  ```typescript
287
- // Multiple workers in the same group - they share/split messages
288
- // Using the same consumer group name means they will load balance messages
289
- await receive("my-topic", "workers", handler1);
290
- await receive("my-topic", "workers", handler2);
291
- // handler1 and handler2 will receive different messages (load balancing)
292
-
293
- // Different consumer groups - each gets copies of ALL messages
294
- await receive("my-topic", "analytics", analyticsHandler);
295
- await receive("my-topic", "webhooks", webhooksHandler);
296
- // analyticsHandler and webhooksHandler will both receive every message
243
+ export const POST = handleCallback(
244
+ async (message, metadata) => {
245
+ await processMessage(message);
246
+ },
247
+ {
248
+ retry: (error, metadata) => {
249
+ if (error instanceof RateLimitError) return { afterSeconds: 60 };
250
+ // Return undefined to let the error propagate normally
251
+ },
252
+ },
253
+ );
297
254
  ```
298
255
 
299
- ## Architecture
300
-
301
- - **Topics**: Named message channels with configurable serialization
302
- - **Consumer Groups**: Named groups of consumers that process messages in
303
- parallel
304
- - `receive()`: Process messages with flexible consumption patterns
305
- - Basic usage: Process next available message
306
- - With `messageId`: Process specific message by ID
307
- - With `skipPayload: true`: Process message metadata only (without payload)
308
- - **Transports**: Pluggable serialization/deserialization for different data
309
- types
310
- - **Streaming**: Memory-efficient processing of large payloads
311
- - **Visibility Timeouts**: Automatic message lifecycle management
312
-
313
- ## Performance
314
-
315
- The multipart parser is optimized for high-throughput scenarios:
256
+ 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.
316
257
 
317
- - **Streaming**: Messages are yielded immediately as headers are parsed
318
- - **Memory Efficient**: No buffering of complete payloads
319
- - **Fast Parsing**: Native Buffer operations for ~50% performance improvement
320
- - **Scalable**: Can handle arbitrarily large responses without memory
321
- constraints
258
+ **Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
322
259
 
323
- ## Serialization (Transport) System
324
-
325
- The queue client supports customizable serialization through the `Transport`
326
- interface with **streaming support** for memory-efficient processing. Transport
327
- can be configured using the `transport` option when calling `send()` or `receive()`.
328
-
329
- ### Built-in Transports
330
-
331
- #### JsonTransport (Default)
260
+ ```typescript
261
+ export const POST = handleCallback(
262
+ async (message, metadata) => {
263
+ await processMessage(message);
264
+ },
265
+ {
266
+ retry: (error, metadata) => {
267
+ // 5s → 10s → 20s → 40s → ... capped at 5 min
268
+ const delay = Math.min(300, 2 ** metadata.deliveryCount * 5);
269
+ return { afterSeconds: delay };
270
+ },
271
+ },
272
+ );
273
+ ```
332
274
 
333
- Buffers data for JSON parsing - suitable for structured data that fits in
334
- memory.
275
+ **Conditional retry** only retry transient errors:
335
276
 
336
277
  ```typescript
337
- import { send, JsonTransport } from "@vercel/queue";
338
-
339
- // JsonTransport is the default, so these are equivalent:
340
- await send("json-topic", { data: "example" });
341
- await send(
342
- "json-topic",
343
- { data: "example" },
344
- { transport: new JsonTransport() },
278
+ export const POST = handleCallback(
279
+ async (message, metadata) => {
280
+ await processMessage(message);
281
+ },
282
+ {
283
+ retry: (error, metadata) => {
284
+ if (error instanceof RateLimitError) return { afterSeconds: 60 };
285
+ if (error instanceof TemporaryError) return { afterSeconds: 30 };
286
+ // Permanent errors: return undefined → retried at the default interval
287
+ },
288
+ },
345
289
  );
346
290
  ```
347
291
 
348
- #### BufferTransport
292
+ **Acknowledging poison messages** — stop retrying messages that can never succeed:
349
293
 
350
- Buffers the entire payload into memory as a Buffer - suitable for binary data
351
- that fits in memory.
294
+ ```typescript
295
+ export const POST = handleCallback(
296
+ async (message, metadata) => {
297
+ await processMessage(message);
298
+ },
299
+ {
300
+ retry: (error, metadata) => {
301
+ if (error instanceof ValidationError) return { acknowledge: true };
302
+ if (metadata.deliveryCount > 5) return { acknowledge: true };
303
+ return { afterSeconds: Math.min(300, 2 ** metadata.deliveryCount * 5) };
304
+ },
305
+ },
306
+ );
307
+ ```
352
308
 
353
- #### StreamTransport
309
+ The `retry` option is available on `handleCallback`, `handleNodeCallback`, and `receive`.
354
310
 
355
- **True streaming support** - passes ReadableStream directly without buffering.
356
- Ideal for large files and memory-efficient processing.
311
+ ## Custom Client Configuration
357
312
 
358
- ### Custom Transport
313
+ All configuration lives on the `QueueClient`:
359
314
 
360
- You can create your own serialization format by implementing the `Transport`
361
- interface.
315
+ ```typescript
316
+ import { QueueClient, BufferTransport } from "@vercel/queue";
317
+
318
+ const queue = new QueueClient({
319
+ region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
320
+ token: "my-token", // Auth token (default: OIDC auto-detection)
321
+ transport: new BufferTransport(), // Serialization (default: JsonTransport)
322
+ headers: { "X-Custom": "header" }, // Custom headers on all requests
323
+ deploymentId: null, // null = unpinned, omit = auto from env, or explicit string
324
+ });
362
325
 
363
- ### Choosing the Right Transport
326
+ // Use directly
327
+ await queue.send("my-topic", myBuffer);
364
328
 
365
- | Use Case | Recommended Transport | Memory Usage | Performance |
366
- | ---------------------- | --------------------- | ------------ | ----------- |
367
- | Small JSON objects | `JsonTransport` | Low | High |
368
- | Binary files < 100MB | `BufferTransport` | Medium | High |
369
- | Large files > 100MB | `StreamTransport` | Very Low | Medium |
370
- | Real-time data streams | `StreamTransport` | Very Low | High |
371
- | Custom protocols | Custom implementation | Varies | Varies |
329
+ // Or destructure
330
+ export const { send, receive, handleCallback, handleNodeCallback } = queue;
331
+ ```
372
332
 
373
- ## API Reference
333
+ 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.
374
334
 
375
- ### Send Function
335
+ To customize the URL scheme, provide a `resolveBaseUrl`:
376
336
 
377
337
  ```typescript
378
- // Simple send - automatically uses default client and JSON transport
379
- await send<T>(topicName, payload);
380
-
381
- // Send with options including custom transport
382
- await send<T>(topicName, payload, {
383
- transport?: Transport<T>;
384
- idempotencyKey?: string;
385
- retentionSeconds?: number;
338
+ const queue = new QueueClient({
339
+ region: process.env.QUEUE_REGION!,
340
+ resolveBaseUrl: (region) => `https://${region}.my-proxy.example`,
386
341
  });
342
+ ```
387
343
 
388
- // Examples:
389
- await send("notifications", { userId: "123", message: "Welcome!" });
390
-
391
- await send("images", imageBuffer, {
392
- transport: new BufferTransport(),
393
- });
344
+ ## Transports
394
345
 
395
- await send("events", eventData, {
396
- idempotencyKey: "unique-key-123",
397
- retentionSeconds: 3600,
398
- });
399
- ```
346
+ The transport controls how message payloads are serialized and deserialized.
400
347
 
401
- ### Receive Function
348
+ | Use Case | Transport | Memory Usage | Notes |
349
+ | --------------- | ----------------- | ------------ | ----------------------- |
350
+ | Structured data | `JsonTransport` | Low | Default, JSON encoding |
351
+ | Binary data | `BufferTransport` | Medium | Raw bytes |
352
+ | Large payloads | `StreamTransport` | Very Low | No buffering, streaming |
402
353
 
403
354
  ```typescript
404
- // Process next available message (simplest form)
405
- await receive("topic-name", "consumer-group", handler);
355
+ import {
356
+ QueueClient,
357
+ JsonTransport,
358
+ BufferTransport,
359
+ StreamTransport,
360
+ } from "@vercel/queue";
406
361
 
407
- // Process specific message by ID with payload
408
- await receive("topic-name", "consumer-group", handler, {
409
- messageId: "message-id",
362
+ // JSON with custom serialization
363
+ const queue = new QueueClient({
364
+ region: process.env.QUEUE_REGION!,
365
+ transport: new JsonTransport({
366
+ replacer: (key, value) => (key === "password" ? undefined : value),
367
+ reviver: (key, value) => (key === "date" ? new Date(value) : value),
368
+ }),
410
369
  });
411
370
 
412
- // Process specific message by ID without payload (metadata only)
413
- // handler will be called with `undefined` as the payload
414
- await receive("topic-name", "consumer-group", handler, {
415
- messageId: "message-id",
416
- skipPayload: true,
371
+ // Binary data
372
+ const binQueue = new QueueClient({
373
+ region: process.env.QUEUE_REGION!,
374
+ transport: new BufferTransport(),
417
375
  });
418
- ```
376
+ await binQueue.send("binary-topic", myBuffer);
419
377
 
420
- ### Message Handler
378
+ // Streaming for large payloads
379
+ const streamQueue = new QueueClient({
380
+ region: process.env.QUEUE_REGION!,
381
+ transport: new StreamTransport(),
382
+ });
383
+ await streamQueue.send("large-file", myReadableStream);
384
+ ```
421
385
 
422
- ```typescript
423
- // Handler function signature
424
- type MessageHandler<T = unknown> = (
425
- message: T,
426
- metadata: MessageMetadata,
427
- ) => Promise<MessageHandlerResult> | MessageHandlerResult;
386
+ ## Manual Receive
428
387
 
429
- // Handler result types
430
- type MessageHandlerResult = void | MessageTimeoutResult;
388
+ 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.
431
389
 
432
- interface MessageTimeoutResult {
433
- timeoutSeconds: number; // seconds before message becomes available again
434
- }
390
+ ### Region considerations
435
391
 
436
- // Message Metadata
437
- interface MessageMetadata {
438
- messageId: string;
439
- deliveryCount: number;
440
- timestamp: string;
441
- }
442
- ```
392
+ 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.
443
393
 
444
- ### Receive Options
394
+ ```bash
395
+ # .env.production — fixed region for manual receive workflows
396
+ QUEUE_REGION=iad1
445
397
 
446
- ```typescript
447
- // Options for the receive function
448
- interface ReceiveOptions<T = unknown> {
449
- messageId?: string; // Process specific message by ID
450
- skipPayload?: boolean; // Skip payload download (requires messageId)
451
- transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
452
- visibilityTimeoutSeconds?: number; // Message visibility timeout
453
- refreshInterval?: number; // Refresh interval for long-running operations
454
- }
398
+ # .env.development
399
+ QUEUE_REGION=iad1
455
400
  ```
456
401
 
457
- ### Transport Interface
402
+ 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).
458
403
 
459
- ```typescript
460
- interface Transport<T = unknown> {
461
- serialize(value: T): Buffer | ReadableStream<Uint8Array>;
462
- deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
463
- contentType: string;
464
- }
465
- ```
404
+ 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.
466
405
 
467
- ### Callback Handler
406
+ ### Usage
468
407
 
469
408
  ```typescript
470
- // Create a callback handler for Next.js route handlers
471
- function handleCallback(
472
- handlers: CallbackHandlers,
473
- ): (request: Request) => Promise<Response>;
474
-
475
- // Configuration object with handlers for different topics and consumer groups
476
- type CallbackHandlers = {
477
- [topicName: string]: { [consumerGroup: string]: MessageHandler };
478
- };
479
-
480
- // Example usage:
481
- export const POST = handleCallback({
482
- "user-events": {
483
- welcome: (message, metadata) => {
484
- console.log(`New user event:`, message, metadata);
485
- },
486
- },
409
+ import { QueueClient } from "@vercel/queue";
487
410
 
488
- // Multiple consumer groups per topic
489
- "image-processing": {
490
- compress: (message, metadata) => console.log("Compressing image", message),
491
- resize: (message, metadata) => console.log("Resizing image", message),
492
- },
493
- });
494
- ```
495
-
496
- ## Examples
411
+ const { receive } = new QueueClient({ region: "iad1" });
497
412
 
498
- ### Basic JSON Processing
499
-
500
- ```typescript
501
- interface UserEvent {
502
- userId: string;
503
- action: string;
504
- timestamp: number;
413
+ // Process next available message
414
+ const result = await receive(
415
+ "my-topic",
416
+ "my-group",
417
+ async (message, metadata) => {
418
+ console.log("Processing:", message);
419
+ },
420
+ );
421
+ if (!result.ok) {
422
+ console.log("Queue was empty:", result.reason);
505
423
  }
506
424
 
507
- // Send a message
508
- await send<UserEvent>("user-events", {
509
- userId: "123",
510
- action: "login",
511
- timestamp: Date.now(),
512
- });
425
+ // Batch processing: up to 10 messages in one request
426
+ await receive("my-topic", "my-group", handler, { limit: 10 });
513
427
 
514
- // Receive and process a message
515
- try {
516
- await receive<UserEvent>("user-events", "processors", async (message) => {
517
- console.log(`User ${message.userId} performed ${message.action}`);
518
- });
519
- } catch (error) {
520
- console.error("Processing error:", error);
521
- }
428
+ // Process a specific message by ID
429
+ await receive("my-topic", "my-group", handler, { messageId: "msg-123" });
522
430
  ```
523
431
 
524
- ### Processing Specific Messages by ID
432
+ > **Note:** `limit` and `messageId` are mutually exclusive options. The handler is never called when the queue is empty — check `result.ok` instead.
433
+
434
+ ## Error Handling
525
435
 
526
436
  ```typescript
527
- // Process a specific message if you know its ID
528
- const messageId = "01234567-89ab-cdef-0123-456789abcdef";
437
+ import {
438
+ BadRequestError,
439
+ DuplicateMessageError,
440
+ ForbiddenError,
441
+ InternalServerError,
442
+ UnauthorizedError,
443
+ } from "@vercel/queue";
444
+ import { send } from "@/lib/queue";
529
445
 
530
446
  try {
531
- await receive<{ userId: string; action: string }>(
532
- "user-events",
533
- "processors",
534
- async (message, { messageId }) => {
535
- console.log(`Processing specific message: ${messageId}`);
536
- console.log(`User ${message.userId} performed ${message.action}`);
537
- },
538
- { messageId },
539
- );
540
- console.log("Message processed successfully");
447
+ await send("my-topic", payload);
541
448
  } catch (error) {
542
- if (error.message.includes("not found or not available")) {
543
- console.log("Message was already processed or does not exist");
544
- } else {
545
- console.error("Error processing message:", error);
449
+ if (error instanceof UnauthorizedError) {
450
+ console.log("Invalid token - refresh authentication");
451
+ } else if (error instanceof ForbiddenError) {
452
+ console.log("Environment mismatch - check configuration");
453
+ } else if (error instanceof BadRequestError) {
454
+ console.log("Invalid parameters:", error.message);
455
+ } else if (error instanceof DuplicateMessageError) {
456
+ console.log("Duplicate message:", error.idempotencyKey);
457
+ } else if (error instanceof InternalServerError) {
458
+ console.log("Server error - retry with backoff");
546
459
  }
547
460
  }
548
461
  ```
549
462
 
550
- ### Processing Next Available Message
463
+ All error types:
464
+
465
+ | Error | Description |
466
+ | ------------------------------------ | --------------------------------------------- |
467
+ | `BadRequestError` | Invalid request parameters |
468
+ | `UnauthorizedError` | Authentication failed (invalid/missing token) |
469
+ | `ForbiddenError` | Access denied (wrong environment/project) |
470
+ | `DuplicateMessageError` | Idempotency key already used |
471
+ | `ConsumerDiscoveryError` | Could not reach consumer deployment |
472
+ | `ConsumerRegistryNotConfiguredError` | Project not configured for queues |
473
+ | `InternalServerError` | Unexpected server error |
474
+ | `InvalidLimitError` | Batch limit outside valid range (1-10) |
475
+ | `MessageNotFoundError` | Message doesn't exist or expired |
476
+ | `MessageNotAvailableError` | Message exists but cannot be claimed |
477
+ | `MessageAlreadyProcessedError` | Message already successfully processed |
478
+ | `MessageLockedError` | Message being processed by another consumer |
479
+ | `MessageCorruptedError` | Message data could not be parsed |
480
+ | `QueueEmptyError` | No messages available in queue |
481
+
482
+ ## Environment Variables
483
+
484
+ | Variable | Description | Default |
485
+ | ---------------------- | ----------------------------------------------- | ------- |
486
+ | `QUEUE_REGION` | Region code for the queue client (user-defined) | - |
487
+ | `VERCEL_REGION` | Current region (auto-set by Vercel) | - |
488
+ | `VERCEL_QUEUE_DEBUG` | Enable debug logging (`1` or `true`) | - |
489
+ | `VERCEL_DEPLOYMENT_ID` | Deployment ID (auto-set by Vercel) | - |
490
+
491
+ ## Service Limits & Constraints
492
+
493
+ ### Throughput & Storage
494
+
495
+ | Limit | Value | Notes |
496
+ | --------------------------- | --------------------- | ----------------------------------- |
497
+ | Message throughput | 10,000+ msg/sec/topic | Scales horizontally |
498
+ | Payload size | 1 GB | Smaller messages have lower latency |
499
+ | Number of topics | Unlimited | No hard limit |
500
+ | Consumer groups per message | ~4,000 | Per-message limit |
501
+ | Messages per queue | Unlimited | No hard limit |
502
+
503
+ ### Parameter Constraints
504
+
505
+ #### Publishing Messages
506
+
507
+ | Parameter | Default | Min | Max | Notes |
508
+ | ------------------ | ------------ | --- | ----------- | ----------------------------------- |
509
+ | `retentionSeconds` | 86,400 (24h) | 60 | 86,400 | Message TTL |
510
+ | `delaySeconds` | 0 | 0 | ≤ retention | Cannot exceed retention |
511
+ | `idempotencyKey` | — | — | — | Dedup window: `min(retention, 24h)` |
512
+
513
+ #### Receiving Messages
514
+
515
+ | Parameter | Default | Min | Max | Notes |
516
+ | -------------------------- | ------- | --- | ----- | ------------------------------- |
517
+ | `visibilityTimeoutSeconds` | 300 | 30 | 3,600 | Lock duration during processing |
518
+ | `limit` | 1 | 1 | 10 | Messages per request |
519
+
520
+ ### Identifier Formats
521
+
522
+ | Identifier | Pattern | Example |
523
+ | -------------- | ---------------- | ----------------------------------- |
524
+ | Topic name | `[A-Za-z0-9_-]+` | `my-queue`, `task_queue_v2` |
525
+ | Consumer group | `[A-Za-z0-9_-]+` | `worker-1`, `analytics_consumer` |
526
+ | Message ID | Opaque string | `0-1`, `3-7K9mNpQrS` |
527
+ | Receipt handle | Opaque string | Used for acknowledge/visibility ops |
528
+
529
+ ### Wildcard Topics
551
530
 
552
- ```typescript
553
- // Process the next available message (one-shot processing)
554
- try {
555
- await receive<{ taskType: string; data: any }>(
556
- "work-queue",
557
- "workers",
558
- async (message) => {
559
- console.log(`Processing task: ${message.taskType}`);
560
- await processTask(message.taskType, message.data);
561
- },
562
- );
563
- console.log("Message processed successfully");
564
- } catch (error) {
565
- if (error instanceof QueueEmptyError) {
566
- console.log("No messages available");
567
- } else if (error instanceof MessageLockedError) {
568
- console.log("Next message is locked");
569
- if (error.retryAfter) {
570
- console.log(`Retry after ${error.retryAfter} seconds`);
531
+ ```json
532
+ {
533
+ "functions": {
534
+ "app/api/queue/route.ts": {
535
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
571
536
  }
572
- } else {
573
- console.error("Error processing message:", error);
574
537
  }
575
538
  }
539
+ ```
576
540
 
577
- // Handle conditional timeouts
578
- await receive<{ taskType: string; data: any }>(
579
- "work-queue",
580
- "workers",
581
- async (message) => {
582
- if (!canProcessTaskType(message.taskType)) {
583
- // Return timeout to retry later
584
- return { timeoutSeconds: 60 };
585
- }
586
-
587
- await processTask(message.taskType, message.data);
588
- },
589
- );
541
+ - `*` may only appear **once** in the pattern
542
+ - `*` must be at the **end** of the topic name
543
+ - Valid: `user-*`, `orders-*`
544
+ - Invalid: `*-events`, `user-*-data`
590
545
 
591
- // Process specific message metadata only (no payload download)
592
- await receive<{ taskType: string; data: any }>(
593
- "work-queue",
594
- "workers",
595
- async (_, metadata) => {
596
- console.log(`Message ID: ${metadata.messageId}`);
597
- console.log(`Delivery count: ${metadata.deliveryCount}`);
598
- console.log(`Timestamp: ${metadata.timestamp}`);
599
- // _ is undefined - no payload was downloaded
600
- },
601
- { messageId: "specific-message-id", skipPayload: true },
602
- );
603
- ```
546
+ ## API Reference
604
547
 
605
- ### Timing Out Messages
548
+ ### `QueueClient`
606
549
 
607
550
  ```typescript
608
- // Process a message with conditional timeout
609
- try {
610
- await receive<{ taskType: string; data: any }>(
611
- "work-queue",
612
- "workers",
613
- async ({ taskType, data }) => {
614
- // Check if we can process this task type right now
615
- if (taskType === "heavy-computation" && isSystemOverloaded()) {
616
- // Return timeout to retry later (5 minutes)
617
- return { timeoutSeconds: 300 };
618
- }
619
-
620
- // Check if we have required resources
621
- if (taskType === "external-api" && !isExternalServiceAvailable()) {
622
- // Return timeout to retry in 1 minute
623
- return { timeoutSeconds: 60 };
624
- }
625
-
626
- // Process the message normally
627
- console.log(`Processing ${taskType} task`);
628
- await processTask(taskType, data);
629
- // Message will be automatically deleted on successful completion
630
- },
631
- );
632
- } catch (error) {
633
- console.error("Worker processing error:", error);
634
- }
551
+ import { QueueClient } from "@vercel/queue";
552
+
553
+ const queue = new QueueClient({
554
+ region: process.env.QUEUE_REGION!, // Required — see Quick Start for env setup
555
+ resolveBaseUrl: (r) => `https://${r}.vercel-queue.com`, // Default resolver
556
+ token: "my-token", // Auto-fetched via OIDC if omitted
557
+ headers: { "X-Custom": "value" },
558
+ transport: new JsonTransport(), // Default: JsonTransport
559
+ deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
560
+ });
635
561
 
636
- // Example with exponential backoff
637
- try {
638
- await receive<{ taskType: string; data: any }>(
639
- "work-queue",
640
- "workers",
641
- async (message, { deliveryCount }) => {
642
- const maxRetries = 3;
643
-
644
- try {
645
- await processMessage(message);
646
- // Successful processing - message will be deleted
647
- } catch (error) {
648
- if (deliveryCount < maxRetries) {
649
- // Exponential backoff: 2^deliveryCount minutes
650
- const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
651
- console.log(
652
- `Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
653
- );
654
- return { timeoutSeconds: timeoutSeconds };
655
- } else {
656
- // Max retries reached, let the message fail and be deleted
657
- console.error(
658
- "Max retries reached, message will be discarded:",
659
- error,
660
- );
661
- throw error;
662
- }
663
- }
664
- },
665
- );
666
- } catch (error) {
667
- console.error("Backoff processing error:", error);
668
- }
562
+ // Methods (arrow functions — safe to destructure)
563
+ const { send, receive, handleCallback, handleNodeCallback } = queue;
669
564
  ```
670
565
 
671
- ## Error Handling
566
+ ### `send(topicName, payload, options?)`
672
567
 
673
- The queue client provides specific error types for different failure scenarios:
568
+ Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
674
569
 
675
- ### Error Types
676
-
677
- - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
678
- empty queue (204 status)
679
-
680
- - Thrown by `receive()` when no messages are available
681
-
682
- - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
683
- status)
684
-
685
- - Contains optional `retryAfter` property with seconds to wait before retry
686
- - For `receive()` without options: the next message is locked
687
- - For `receive()` with messageId: the requested message is locked
688
-
689
- - **`MessageNotFoundError`**: Message doesn't exist (404 status)
570
+ ```typescript
571
+ const { messageId } = await send("my-topic", payload, {
572
+ idempotencyKey: "unique-key", // Dedup window: min(retention, 24h)
573
+ retentionSeconds: 3600, // Message TTL (default: 86400)
574
+ delaySeconds: 60, // Delay before visible (default: 0)
575
+ headers: { "X-Custom": "val" }, // Custom headers
576
+ });
577
+ ```
690
578
 
691
- - **`MessageNotAvailableError`**: Message exists but isn't available for
692
- processing (409 status)
579
+ ### `receive(topicName, consumerGroup, handler, options?)`
693
580
 
694
- - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
581
+ 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.
695
582
 
696
- - **`BadRequestError`**: Invalid request parameters (400 status)
583
+ For receive-by-id, operational errors are returned instead of thrown:
697
584
 
698
- - Invalid queue names, missing required parameters
585
+ ```typescript
586
+ const result = await receive("my-topic", "my-group", handler, {
587
+ messageId: "msg-123",
588
+ });
589
+ if (!result.ok) {
590
+ // result.reason is "not_found" | "not_available" | "already_processed"
591
+ console.log(result.reason, result.messageId);
592
+ }
593
+ ```
699
594
 
700
- - **`UnauthorizedError`**: Authentication failure (401 status)
595
+ ```typescript
596
+ // Batch mode
597
+ const result = await receive("my-topic", "my-group", handler, {
598
+ limit: 10, // Max messages (default: 1, max: 10)
599
+ visibilityTimeoutSeconds: 60, // Lock duration (default: 300)
600
+ });
601
+ ```
701
602
 
702
- - Missing or invalid authentication token
603
+ ### `handleCallback(handler, options?)`
703
604
 
704
- - **`ForbiddenError`**: Access denied (403 status)
605
+ Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
705
606
 
706
- - Queue environment doesn't match token environment
607
+ ```typescript
608
+ export const POST = handleCallback(
609
+ async (message, metadata) => {
610
+ await processMessage(message);
611
+ },
612
+ {
613
+ visibilityTimeoutSeconds: 300, // Lock duration (default: 300)
614
+ retry: (error, metadata) => {
615
+ // Optional: return { afterSeconds: N } to reschedule, { acknowledge: true } to ack, or undefined to propagate
616
+ },
617
+ },
618
+ );
619
+ ```
707
620
 
708
- - **`InternalServerError`**: Server-side errors (500+ status codes)
709
- - Unexpected server errors, service unavailable, etc.
621
+ ### `handleNodeCallback(handler, options?)`
710
622
 
711
- ### Error Handling Examples
623
+ Vercel only. Returns `(req, res) => Promise<void>` — for frameworks that export Connect-style handlers.
712
624
 
713
625
  ```typescript
714
- import {
715
- BadRequestError,
716
- ForbiddenError,
717
- InternalServerError,
718
- MessageLockedError,
719
- QueueEmptyError,
720
- UnauthorizedError,
721
- } from "@vercel/queue";
722
-
723
- // Handle empty queue or locked messages
724
- try {
725
- await receive("my-topic", "my-consumer", async (message) => {
726
- // Process message
727
- console.log("Processing message:", message);
728
- });
729
- } catch (error) {
730
- if (error instanceof QueueEmptyError) {
731
- console.log("Queue is empty, retry later");
732
- } else if (error instanceof MessageLockedError) {
733
- console.log("Next message is locked");
734
- if (error.retryAfter) {
735
- console.log(`Retry after ${error.retryAfter} seconds`);
736
- }
737
- }
738
- }
626
+ // pages/api/queue/my-topic.ts
627
+ export default handleNodeCallback(
628
+ async (message, metadata) => {
629
+ await processMessage(message);
630
+ },
631
+ {
632
+ retry: (error, metadata) => ({ afterSeconds: 60 }),
633
+ },
634
+ );
635
+ ```
739
636
 
740
- // Handle locked message with retry
741
- try {
742
- await receive("my-topic", "my-consumer", handler, { messageId });
743
- } catch (error) {
744
- if (error instanceof MessageLockedError) {
745
- console.log("Message is locked by another consumer");
746
- if (error.retryAfter) {
747
- console.log(`Retry after ${error.retryAfter} seconds`);
748
- setTimeout(() => retry(), error.retryAfter * 1000);
749
- }
750
- }
751
- }
637
+ ### Handler Signature
752
638
 
753
- // Handle authentication and authorization errors
754
- try {
755
- await send("my-topic", payload);
756
- } catch (error) {
757
- if (error instanceof UnauthorizedError) {
758
- console.log("Invalid token - refresh authentication");
759
- } else if (error instanceof ForbiddenError) {
760
- console.log("Environment mismatch - check token/queue configuration");
761
- } else if (error instanceof BadRequestError) {
762
- console.log("Invalid parameters:", error.message);
763
- } else if (error instanceof InternalServerError) {
764
- console.log("Server error - retry with backoff");
765
- }
766
- }
639
+ ```typescript
640
+ type MessageHandler<T> = (
641
+ message: T,
642
+ metadata: MessageMetadata,
643
+ ) => Promise<void> | void;
767
644
 
768
- // Complete error handling pattern
769
- function handleQueueError(error: unknown): void {
770
- if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
771
- // Transient errors - safe to retry
772
- console.log("Temporary condition, will retry");
773
- } else if (
774
- error instanceof UnauthorizedError ||
775
- error instanceof ForbiddenError
776
- ) {
777
- // Authentication/authorization errors - need to fix configuration
778
- console.log("Auth error - check credentials");
779
- } else if (error instanceof BadRequestError) {
780
- // Client error - fix the request
781
- console.log("Invalid request:", error.message);
782
- } else if (error instanceof InternalServerError) {
783
- // Server error - implement exponential backoff
784
- console.log("Server error - retry with backoff");
785
- } else {
786
- // Unknown error
787
- console.error("Unexpected error:", error);
788
- }
645
+ interface MessageMetadata {
646
+ messageId: string;
647
+ deliveryCount: number;
648
+ createdAt: Date;
649
+ expiresAt?: Date;
650
+ topicName: string;
651
+ consumerGroup: string;
652
+ region: string;
789
653
  }
790
654
  ```
791
655