@vercel/queue 0.0.0-alpha.39 → 0.0.0-alpha.40

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
@@ -4,14 +4,12 @@ A TypeScript client library for interacting with the Vercel Queue Service API, d
4
4
 
5
5
  ## Features
6
6
 
7
- - **Automatic Queue Triggering**: Vercel automatically triggers your API routes when messages are ready
8
- - **Next.js Integration**: Built-in support for Next.js App Router and Pages Router
9
- - **Generic Payload Support**: Send and receive any type of data with type safety
10
- - **Pub/Sub Pattern**: Topic-based messaging with consumer groups
11
- - **Type Safety**: Full TypeScript support with generic types
12
- - **Streaming Support**: Handle large payloads efficiently
13
- - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
14
- - **Framework Adapters**: Web API, Next.js App Router, and Pages Router support
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
15
13
 
16
14
  ## Installation
17
15
 
@@ -21,58 +19,93 @@ npm install @vercel/queue
21
19
 
22
20
  ## Quick Start
23
21
 
24
- For local development, you'll need to set up your Vercel project:
22
+ Set up your region via environment variables. If your framework supports `.env` files (Next.js, Vite, Nuxt, etc.):
25
23
 
26
24
  ```bash
27
- # Install Vercel CLI if you haven't already
28
- npm i -g vercel
25
+ # .env.production (on Vercel, inherits the platform's region)
26
+ QUEUE_REGION=${VERCEL_REGION}
29
27
 
30
- # Link your project to Vercel
31
- vc link
28
+ # .env.development (fixed region for local dev — iad1 is recommended)
29
+ QUEUE_REGION=iad1
30
+ ```
32
31
 
33
- # Pull environment variables from your Vercel project
34
- vc env pull
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:
35
+
36
+ ```typescript
37
+ // lib/queue.ts
38
+ import { QueueClient } from "@vercel/queue";
39
+
40
+ const queue = new QueueClient({ region: process.env.QUEUE_REGION! });
41
+ export const { send, receive, handleCallback, handleNodeCallback } = queue;
35
42
  ```
36
43
 
37
- ## Local Development
44
+ Send a message anywhere in your app:
38
45
 
39
- **Queues just work locally.** After you have setup your Vercel project, when you `send()` messages in development mode, they automatically trigger your handlers locally - no external queue infrastructure needed.
46
+ ```typescript
47
+ import { send } from "@/lib/queue";
40
48
 
41
- The library reads your `vercel.json` configuration, discovers your queue handlers, and triggers them automatically when messages are sent.
49
+ await send("my-topic", { message: "Hello world" });
50
+ ```
42
51
 
43
- > **Note:** Local dev mode is enabled when `NODE_ENV=development`. Most frameworks (Next.js, etc.) set this automatically when running `npm run dev`.
52
+ Handle incoming messages with a route handler:
44
53
 
45
- ### Example Workflow
54
+ ```typescript
55
+ // app/api/queue/my-topic/route.ts
56
+ import { handleCallback } from "@/lib/queue";
46
57
 
47
- ```bash
48
- # Start your dev server
49
- npm run dev
58
+ export const POST = handleCallback(async (message, metadata) => {
59
+ console.log("Processing:", message);
60
+ });
61
+ ```
50
62
 
51
- # Send messages - they process locally automatically!
63
+ Configure your `vercel.json`:
64
+
65
+ ```json
66
+ {
67
+ "functions": {
68
+ "app/api/queue/my-topic/route.ts": {
69
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "my-topic" }]
70
+ }
71
+ }
72
+ }
52
73
  ```
53
74
 
54
- ### Publishing Messages
75
+ ### Project Setup
55
76
 
56
- The `send` function can be used anywhere in your codebase to publish messages to a queue:
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
57
92
 
58
93
  ```typescript
59
- import { send } from "@vercel/queue";
94
+ import { QueueClient } from "@vercel/queue";
60
95
 
61
- // Send a message to a topic
62
- await send("my-topic", {
63
- message: "Hello world",
64
- });
96
+ const { send } = new QueueClient({ region: process.env.QUEUE_REGION! });
65
97
 
66
- // With additional options
98
+ // Simple send
99
+ await send("my-topic", { message: "Hello world" });
100
+
101
+ // With options
67
102
  await send(
68
103
  "my-topic",
104
+ { message: "Hello world" },
69
105
  {
70
- message: "Hello world",
71
- },
72
- {
73
- idempotencyKey: "unique-key", // Optional: prevent duplicate messages
74
- retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
75
- delaySeconds: 60, // Optional: delay message delivery by N seconds
106
+ idempotencyKey: "unique-key", // Prevent duplicate messages
107
+ retentionSeconds: 3600, // 1 hour TTL (default: 24h)
108
+ delaySeconds: 60, // Delay delivery by 1 minute
76
109
  },
77
110
  );
78
111
  ```
@@ -81,41 +114,37 @@ Example usage in an API route:
81
114
 
82
115
  ```typescript
83
116
  // app/api/send-message/route.ts
84
- import { send } from "@vercel/queue";
117
+ import { send } from "@/lib/queue";
85
118
 
86
119
  export async function POST(request: Request) {
87
120
  const body = await request.json();
88
-
89
- const { messageId } = await send("my-topic", {
90
- message: body.message,
91
- });
92
-
121
+ const { messageId } = await send("my-topic", { message: body.message });
93
122
  return Response.json({ messageId });
94
123
  }
95
124
  ```
96
125
 
97
- ### Consuming Messages
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.
98
127
 
99
- Messages are consumed using API routes that Vercel automatically triggers when messages are available.
128
+ ## Consuming Messages
100
129
 
101
- #### 1. Create API Routes
130
+ ### On Vercel
102
131
 
103
- ##### Web API (`@vercel/queue/web`)
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.
104
133
 
105
- The `handleCallback` from `@vercel/queue/web` returns a standard `(Request) => Promise<Response>` handler. It works with any framework that uses the Web API `Request`/`Response` types, including Next.js App Router, Hono, and others.
134
+ #### Web API `handleCallback`
135
+
136
+ Returns `(Request) => Promise<Response>`. For frameworks that export Web API route handlers (Next.js App Router, Hono, etc.).
106
137
 
107
138
  **Next.js App Router:**
108
139
 
109
140
  ```typescript
110
141
  // app/api/queue/my-topic/route.ts
111
- import { handleCallback } from "@vercel/queue/web";
142
+ import { handleCallback } from "@/lib/queue";
112
143
 
113
144
  export const POST = handleCallback(async (message, metadata) => {
114
- // metadata includes: { messageId, deliveryCount, createdAt, topicName, consumerGroup }
115
- console.log("Processing message:", message);
116
-
117
- // If this throws an error, the message will be automatically retried
145
+ // metadata: { messageId, deliveryCount, createdAt, expiresAt?, topicName, consumerGroup, region }
118
146
  await processMessage(message);
147
+ // Throwing an error will automatically retry the message
119
148
  });
120
149
  ```
121
150
 
@@ -123,57 +152,53 @@ export const POST = handleCallback(async (message, metadata) => {
123
152
 
124
153
  ```typescript
125
154
  import { Hono } from "hono";
126
- import { handleCallback } from "@vercel/queue/web";
155
+ import { handleCallback } from "@/lib/queue";
127
156
 
128
157
  const app = new Hono();
129
-
130
158
  app.post(
131
159
  "/api/queue",
132
160
  handleCallback(async (message, metadata) => {
133
- console.log("Processing:", message);
161
+ await processMessage(message);
134
162
  }),
135
163
  );
136
-
137
164
  export default app;
138
165
  ```
139
166
 
140
- For multiple topics/consumers, create separate route files:
167
+ #### Connect-style `handleNodeCallback`
141
168
 
142
- ```typescript
143
- // app/api/queue/orders/fulfillment/route.ts
144
- import { handleCallback } from "@vercel/queue/web";
169
+ Returns `(req, res) => Promise<void>`. For frameworks that export Connect-style handlers (Express, Next.js Pages Router, etc.).
145
170
 
146
- export const POST = handleCallback(async (order, metadata) => {
147
- await processOrder(order);
148
- });
149
- ```
171
+ **Next.js Pages Router:**
150
172
 
151
173
  ```typescript
152
- // app/api/queue/orders/analytics/route.ts
153
- import { handleCallback } from "@vercel/queue/web";
174
+ // pages/api/queue/my-topic.ts
175
+ import { handleNodeCallback } from "@/lib/queue";
154
176
 
155
- export const POST = handleCallback(async (order, metadata) => {
156
- await trackOrder(order);
177
+ export default handleNodeCallback(async (message, metadata) => {
178
+ await processMessage(message);
157
179
  });
158
180
  ```
159
181
 
160
- ##### Pages Router (`@vercel/queue/nextjs/pages`)
161
-
162
- For Next.js Pages Router, import from `@vercel/queue/nextjs/pages`. This returns a `(req, res) => Promise<void>` handler:
182
+ **Express:**
163
183
 
164
184
  ```typescript
165
- // pages/api/queue/my-topic.ts
166
- import { handleCallback } from "@vercel/queue/nextjs/pages";
185
+ import express from "express";
186
+ import { handleNodeCallback } from "@/lib/queue";
167
187
 
168
- export default handleCallback(async (message, metadata) => {
169
- console.log("Processing message:", message);
170
- await processMessage(message);
171
- });
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;
172
197
  ```
173
198
 
174
- #### 2. Configure vercel.json
199
+ ### 2. Configure vercel.json
175
200
 
176
- Configure which topics and consumers your API routes handle.
201
+ Tell Vercel which routes handle which topics:
177
202
 
178
203
  ```json
179
204
  {
@@ -190,10 +215,7 @@ Configure which topics and consumers your API routes handle.
190
215
  },
191
216
  "app/api/queue/orders/fulfillment/route.ts": {
192
217
  "experimentalTriggers": [
193
- {
194
- "type": "queue/v2beta",
195
- "topic": "order-events"
196
- }
218
+ { "type": "queue/v2beta", "topic": "order-events" }
197
219
  ]
198
220
  },
199
221
  "app/api/queue/orders/analytics/route.ts": {
@@ -209,152 +231,207 @@ Configure which topics and consumers your API routes handle.
209
231
  }
210
232
  ```
211
233
 
212
- ### Key Concepts
234
+ Multiple route files for the same topic create separate consumer groups — each receives a copy of every message.
213
235
 
214
- - **Topics**: Named message channels that can have multiple consumer groups
215
- - **Consumer Groups**: Named groups of consumers that process messages in parallel
216
- - Different consumer groups for the same topic each get a copy of every message
217
- - Multiple consumers in the same group share/split messages for load balancing
218
- - **Automatic Triggering**: Vercel triggers your API routes when messages are available
219
- - **Message Processing**: Your API routes receive message metadata via headers
220
- - **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
221
- - **Delivery Modes**: The server uses CloudEvents binary content mode to deliver messages. For small messages, the full payload and receipt handle are pushed directly in the HTTP body and headers, avoiding an extra API fetch. For large messages, only the message ID is sent and the SDK fetches the payload.
236
+ ### 3. Retry and Backoff
222
237
 
223
- ## Advanced Features
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).
239
+
240
+ For finer control over retry timing, pass a `retry` option:
241
+
242
+ ```typescript
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
+ );
254
+ ```
224
255
 
225
- ### Custom Client Configuration
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.
226
257
 
227
- For custom configuration (tokens, headers, transport), create a `QueueClient` and pass it via options:
258
+ **Exponential backoff** uses `metadata.deliveryCount` (starts at 1, increments each delivery):
228
259
 
229
260
  ```typescript
230
- import { QueueClient, send } from "@vercel/queue";
231
- import { handleCallback } from "@vercel/queue/web";
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
+ ```
232
274
 
233
- const client = new QueueClient({
234
- token: "my-token",
235
- headers: { "X-Custom": "header" },
236
- });
275
+ **Conditional retry** only retry transient errors:
276
+
277
+ ```typescript
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
+ },
289
+ );
290
+ ```
237
291
 
238
- // Send with custom client
239
- await send("my-topic", { hello: "world" }, { client });
292
+ **Acknowledging poison messages** stop retrying messages that can never succeed:
240
293
 
241
- // Handle callbacks with custom client
242
- export const POST = handleCallback(async (msg, meta) => console.log(msg), {
243
- client,
244
- });
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
+ );
245
307
  ```
246
308
 
247
- ### Core Handler (Framework Agnostic)
309
+ The `retry` option is available on `handleCallback`, `handleNodeCallback`, and `receive`.
310
+
311
+ ## Custom Client Configuration
248
312
 
249
- For custom framework integration, use the core `handleCallback` from `@vercel/queue`. It takes parsed request data and throws on errors:
313
+ All configuration lives on the `QueueClient`:
250
314
 
251
315
  ```typescript
252
- import { handleCallback, parseRawCallback } from "@vercel/queue";
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
+ });
253
325
 
254
- // In your framework handler:
255
- const parsed = parseRawCallback(body, headers);
256
- try {
257
- await handleCallback(async (msg, meta) => {
258
- console.log("Processing:", msg);
259
- }, parsed);
260
- // success
261
- } catch (error) {
262
- // handle error → 500
263
- }
326
+ // Use directly
327
+ await queue.send("my-topic", myBuffer);
328
+
329
+ // Or destructure
330
+ export const { send, receive, handleCallback, handleNodeCallback } = queue;
264
331
  ```
265
332
 
266
- ### Serialization (Transport) System
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.
267
334
 
268
- The queue client supports customizable serialization through the `Transport` interface:
335
+ To customize the URL scheme, provide a `resolveBaseUrl`:
336
+
337
+ ```typescript
338
+ const queue = new QueueClient({
339
+ region: process.env.QUEUE_REGION!,
340
+ resolveBaseUrl: (region) => `https://${region}.my-proxy.example`,
341
+ });
342
+ ```
269
343
 
270
- #### Built-in Transports
344
+ ## Transports
271
345
 
272
- 1. **JsonTransport (Default)**: For structured data that fits in memory
273
- 2. **BufferTransport**: For binary data that fits in memory
274
- 3. **StreamTransport**: For large files and memory-efficient processing
346
+ The transport controls how message payloads are serialized and deserialized.
275
347
 
276
- Example:
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 |
277
353
 
278
354
  ```typescript
279
- import { send, JsonTransport } from "@vercel/queue";
355
+ import {
356
+ QueueClient,
357
+ JsonTransport,
358
+ BufferTransport,
359
+ StreamTransport,
360
+ } from "@vercel/queue";
280
361
 
281
- // JsonTransport is the default
282
- await send("json-topic", { data: "example" });
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
+ }),
369
+ });
283
370
 
284
- // Explicit transport configuration
285
- await send(
286
- "json-topic",
287
- { data: "example" },
288
- { transport: new JsonTransport() },
289
- );
371
+ // Binary data
372
+ const binQueue = new QueueClient({
373
+ region: process.env.QUEUE_REGION!,
374
+ transport: new BufferTransport(),
375
+ });
376
+ await binQueue.send("binary-topic", myBuffer);
290
377
 
291
- // JsonTransport with custom serialization
292
- const transport = new JsonTransport({
293
- replacer: (key, value) => (key === "password" ? undefined : value),
294
- reviver: (key, value) => (key === "date" ? new Date(value) : value),
378
+ // Streaming for large payloads
379
+ const streamQueue = new QueueClient({
380
+ region: process.env.QUEUE_REGION!,
381
+ transport: new StreamTransport(),
295
382
  });
296
- await send("json-topic", { data: "example" }, { transport });
383
+ await streamQueue.send("large-file", myReadableStream);
297
384
  ```
298
385
 
299
- ### Transport Selection Guide
386
+ ## Manual Receive
300
387
 
301
- | Use Case | Recommended Transport | Memory Usage | Performance |
302
- | ------------------ | --------------------- | ------------ | ----------- |
303
- | Small JSON objects | JsonTransport | Low | High |
304
- | Binary data | BufferTransport | Medium | High |
305
- | Large payloads | StreamTransport | Very Low | Medium |
306
- | Real-time streams | StreamTransport | Very Low | High |
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.
307
389
 
308
- ## Handling Empty Queues
390
+ ### Region considerations
309
391
 
310
- When no messages are available in the queue, the handler receives `null` for both the message and metadata parameters. This allows graceful handling without exceptions:
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.
311
393
 
312
- ```typescript
313
- await receive("my-topic", "my-consumer", async (message, metadata) => {
314
- if (!message) {
315
- console.log("No message received - queue is empty");
316
- return;
317
- }
394
+ ```bash
395
+ # .env.production fixed region for manual receive workflows
396
+ QUEUE_REGION=iad1
318
397
 
319
- // Process the message
320
- console.log("Processing:", message);
321
- console.log("Message ID:", metadata.messageId);
322
- });
398
+ # .env.development
399
+ QUEUE_REGION=iad1
323
400
  ```
324
401
 
325
- The same pattern works with `handleCallback`:
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).
403
+
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.
405
+
406
+ ### Usage
326
407
 
327
408
  ```typescript
328
- import { handleCallback } from "@vercel/queue/web";
409
+ import { QueueClient } from "@vercel/queue";
329
410
 
330
- export const POST = handleCallback(async (message, metadata) => {
331
- if (!message) {
332
- // No message available - handle gracefully
333
- return;
334
- }
335
- await processMessage(message);
336
- });
337
- ```
411
+ const { receive } = new QueueClient({ region: "iad1" });
338
412
 
339
- ## Error Handling
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);
423
+ }
424
+
425
+ // Batch processing: up to 10 messages in one request
426
+ await receive("my-topic", "my-group", handler, { limit: 10 });
340
427
 
341
- The queue client provides specific error types:
428
+ // Process a specific message by ID
429
+ await receive("my-topic", "my-group", handler, { messageId: "msg-123" });
430
+ ```
342
431
 
343
- - **`MessageLockedError`**: Message is being processed by another consumer
344
- - **`MessageNotFoundError`**: Message doesn't exist or has expired
345
- - **`MessageNotAvailableError`**: Message exists but cannot be claimed
346
- - **`MessageAlreadyProcessedError`**: Message was already successfully processed
347
- - **`MessageCorruptedError`**: Message data could not be parsed
348
- - **`BadRequestError`**: Invalid request parameters
349
- - **`UnauthorizedError`**: Authentication failed (invalid or missing token)
350
- - **`ForbiddenError`**: Access denied (wrong environment or project)
351
- - **`DuplicateMessageError`**: Idempotency key was already used
352
- - **`ConsumerDiscoveryError`**: Could not reach the consumer deployment
353
- - **`ConsumerRegistryNotConfiguredError`**: Project not configured for queues
354
- - **`InternalServerError`**: Unexpected server error
355
- - **`InvalidLimitError`**: Batch limit outside valid range (1-10)
432
+ > **Note:** `limit` and `messageId` are mutually exclusive options. The handler is never called when the queue is empty — check `result.ok` instead.
356
433
 
357
- Example error handling:
434
+ ## Error Handling
358
435
 
359
436
  ```typescript
360
437
  import {
@@ -364,6 +441,7 @@ import {
364
441
  InternalServerError,
365
442
  UnauthorizedError,
366
443
  } from "@vercel/queue";
444
+ import { send } from "@/lib/queue";
367
445
 
368
446
  try {
369
447
  await send("my-topic", payload);
@@ -382,63 +460,33 @@ try {
382
460
  }
383
461
  ```
384
462
 
385
- ## Environment Variables
386
-
387
- The following environment variables can be used to configure the queue client:
388
-
389
- | Variable | Description | Default |
390
- | ------------------------ | ------------------------------------ | -------------------------- |
391
- | `VERCEL_QUEUE_BASE_URL` | Override the queue service URL | `https://vercel-queue.com` |
392
- | `VERCEL_QUEUE_BASE_PATH` | Override the API base path | `/api/v3/topic` |
393
- | `VERCEL_QUEUE_DEBUG` | Enable debug logging (`1` or `true`) | - |
394
- | `VERCEL_DEPLOYMENT_ID` | Deployment ID (auto-set by Vercel) | - |
395
-
396
- ## Advanced Usage
397
-
398
- ### Direct Message Processing
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 |
399
481
 
400
- > **Note**: The `receive` function is for advanced use cases where you need direct message processing control outside of Vercel's automatic triggering.
401
-
402
- ```typescript
403
- import { receive } from "@vercel/queue";
404
-
405
- // Process next available message (or null if queue is empty)
406
- await receive<T>(topicName, consumerGroup, async (message, metadata) => {
407
- if (!message) {
408
- console.log("Queue is empty");
409
- return;
410
- }
411
- // Process message
412
- });
413
-
414
- // Batch processing: fetch up to 10 messages in one request
415
- await receive<T>(topicName, consumerGroup, handler, {
416
- limit: 10, // Default: 1, Min: 1, Max: 10
417
- });
418
-
419
- // Process specific message by ID
420
- await receive<T>(topicName, consumerGroup, handler, {
421
- messageId: "message-id",
422
- });
482
+ ## Environment Variables
423
483
 
424
- // Note: limit and messageId are mutually exclusive options
425
-
426
- // Handler function signature
427
- // When queue is empty, both message and metadata are null
428
- type MessageHandler<T = unknown> = (
429
- message: T | null,
430
- metadata: MessageMetadata | null,
431
- ) => Promise<void> | void;
432
-
433
- // MessageMetadata type
434
- interface MessageMetadata {
435
- messageId: string;
436
- deliveryCount: number;
437
- createdAt: Date;
438
- topicName: string;
439
- consumerGroup: string;
440
- }
441
- ```
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) | - |
442
490
 
443
491
  ## Service Limits & Constraints
444
492
 
@@ -464,57 +512,32 @@ interface MessageMetadata {
464
512
 
465
513
  #### Receiving Messages
466
514
 
467
- | Parameter | Default | Min | Max | Notes |
468
- | -------------------------- | ------- | --- | ----- | --------------------------- |
469
- | `visibilityTimeoutSeconds` | 30 | 0 | 3,600 | 0 = immediate re-visibility |
470
- | `limit` | 1 | 1 | 10 | Messages per request |
471
-
472
- #### Visibility Extension
473
-
474
- | Constraint | Value |
475
- | -------------------------- | ---------------------------------- |
476
- | `visibilityTimeoutSeconds` | 0 - 3,600 seconds |
477
- | Cannot extend beyond | Message's original expiration time |
478
- | Receipt handle | Must match the receive operation |
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 |
479
519
 
480
520
  ### Identifier Formats
481
521
 
482
- | Identifier | Pattern | Example |
483
- | ---------------- | ---------------- | -------------------------------- |
484
- | Topic/Queue name | `[A-Za-z0-9_-]+` | `my-queue`, `task_queue_v2` |
485
- | Consumer group | `[A-Za-z0-9_-]+` | `worker-1`, `analytics_consumer` |
486
- | Message ID | Opaque string | `0-1`, `3-7K9mNpQrS` |
487
- | Receipt handle | Opaque string | Used for delete/visibility ops |
488
-
489
- ### Content-Type Handling
490
-
491
- | Scenario | Result |
492
- | ------------------------------- | -------------------------- |
493
- | Client provides `Content-Type` | Used as-is |
494
- | No header, magic bytes detected | Auto-detected MIME type |
495
- | No header, detection fails | `application/octet-stream` |
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 |
496
528
 
497
529
  ### Wildcard Topics
498
530
 
499
- Topic patterns support wildcards for flexible routing:
500
-
501
531
  ```json
502
532
  {
503
533
  "functions": {
504
534
  "app/api/queue/route.ts": {
505
- "experimentalTriggers": [
506
- {
507
- "type": "queue/v2beta",
508
- "topic": "user-*"
509
- }
510
- ]
535
+ "experimentalTriggers": [{ "type": "queue/v2beta", "topic": "user-*" }]
511
536
  }
512
537
  }
513
538
  }
514
539
  ```
515
540
 
516
- **Wildcard Rules:**
517
-
518
541
  - `*` may only appear **once** in the pattern
519
542
  - `*` must be at the **end** of the topic name
520
543
  - Valid: `user-*`, `orders-*`
@@ -522,142 +545,111 @@ Topic patterns support wildcards for flexible routing:
522
545
 
523
546
  ## API Reference
524
547
 
525
- ### Export Structure
526
-
527
- | Import Path | `handleCallback` |
528
- | ---------------------------- | ---------------------------------------------------------------- |
529
- | `@vercel/queue` | Core async function: `(handler, parsed, opts?) => Promise<void>` |
530
- | `@vercel/queue/web` | Returns `(request: Request) => Promise<Response>` |
531
- | `@vercel/queue/nextjs/pages` | Returns `(req, res) => Promise<void>` |
532
-
533
- Additional exports from `@vercel/queue`:
534
-
535
- | Export | Description |
536
- | ------------------------- | ------------------------------------------------------------- |
537
- | `parseCallback` | Parse a Web API `Request` into a `ParsedCallbackRequest` |
538
- | `parseRawCallback` | Parse a pre-parsed body + headers (e.g. Pages Router) |
539
- | `CLOUD_EVENT_TYPE_V2BETA` | `"com.vercel.queue.v2beta"` — binary CloudEvent type constant |
540
-
541
- ### QueueClient Configuration
548
+ ### `QueueClient`
542
549
 
543
550
  ```typescript
544
551
  import { QueueClient } from "@vercel/queue";
545
552
 
546
- const client = new QueueClient({
547
- // Base URL for the queue service
548
- // Default: "https://vercel-queue.com"
549
- // Env: VERCEL_QUEUE_BASE_URL
550
- baseUrl: "https://vercel-queue.com",
551
-
552
- // API path prefix
553
- // Default: "/api/v3/topic"
554
- // Env: VERCEL_QUEUE_BASE_PATH
555
- basePath: "/api/v3/topic",
556
-
557
- // Auth token (auto-fetched via OIDC if not provided)
558
- token: "my-token",
559
-
560
- // Custom headers for all requests
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
561
557
  headers: { "X-Custom": "value" },
562
-
563
- // Deployment ID for message routing
564
- // Default: process.env.VERCEL_DEPLOYMENT_ID
565
- deploymentId: "dpl_xxx",
566
-
567
- // Pin messages to current deployment when publishing
568
- // Default: true
569
- pinToDeployment: true,
558
+ transport: new JsonTransport(), // Default: JsonTransport
559
+ deploymentId: undefined, // omit = auto from env (pinned), null = unpinned, or explicit string
570
560
  });
571
561
 
572
- // Pass to any function via options
573
- await send("my-topic", payload, { client });
574
- export const POST = handleCallback(handler, { client });
562
+ // Methods (arrow functions safe to destructure)
563
+ const { send, receive, handleCallback, handleNodeCallback } = queue;
575
564
  ```
576
565
 
577
- ### Send Options
578
-
579
- ```typescript
580
- await send("my-topic", payload, {
581
- // Deduplication key
582
- // Dedup window: min(retentionSeconds, 24 hours)
583
- idempotencyKey: "unique-key",
584
-
585
- // Message TTL in seconds
586
- // Default: 86400, Min: 60, Max: 86400
587
- retentionSeconds: 3600,
566
+ ### `send(topicName, payload, options?)`
588
567
 
589
- // Delay before message becomes visible
590
- // Default: 0, Min: 0, Max: retentionSeconds
591
- delaySeconds: 60,
568
+ Returns `{ messageId: string | null }`. `messageId` is `null` when the server accepted the message for deferred processing (e.g. during a server-side outage).
592
569
 
593
- // Custom serializer (default: JsonTransport)
594
- transport: new JsonTransport(),
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
595
576
  });
596
577
  ```
597
578
 
598
- ### Receive Options
579
+ ### `receive(topicName, consumerGroup, handler, options?)`
580
+
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.
599
582
 
600
- The `receive` function supports two mutually exclusive modes:
583
+ For receive-by-id, operational errors are returned instead of thrown:
601
584
 
602
585
  ```typescript
603
- // Batch mode: receive multiple messages
604
- await receive("my-topic", "my-consumer", handler, {
605
- // Maximum messages to retrieve in a single request
606
- // Default: 1, Min: 1, Max: 10
607
- limit: 10,
608
-
609
- // Message lock duration in seconds
610
- // Default: 300, Min: 30, Max: 3600
611
- visibilityTimeoutSeconds: 60,
586
+ const result = await receive("my-topic", "my-group", handler, {
587
+ messageId: "msg-123",
612
588
  });
589
+ if (!result.ok) {
590
+ // result.reason is "not_found" | "not_available" | "already_processed"
591
+ console.log(result.reason, result.messageId);
592
+ }
593
+ ```
613
594
 
614
- // By-ID mode: receive a specific message
615
- await receive("my-topic", "my-consumer", handler, {
616
- // Specific message ID to consume
617
- messageId: "0-1",
618
-
619
- // Message lock duration in seconds
620
- // Default: 300, Min: 30, Max: 3600
621
- visibilityTimeoutSeconds: 60,
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)
622
600
  });
623
601
  ```
624
602
 
625
- > **Note**: `limit` and `messageId` cannot be used together - they are mutually exclusive options.
603
+ ### `handleCallback(handler, options?)`
626
604
 
627
- ### handleCallback Options
605
+ Vercel only. Returns `(request: Request) => Promise<Response>` — for frameworks that export Web API route handlers.
628
606
 
629
607
  ```typescript
630
- import { handleCallback } from "@vercel/queue/web";
631
-
632
608
  export const POST = handleCallback(
633
609
  async (message, metadata) => {
634
610
  await processMessage(message);
635
611
  },
636
612
  {
637
- // Message lock duration for long-running handlers
638
- // Default: 300, Min: 30, Max: 3600
639
- visibilityTimeoutSeconds: 300, // 5 minutes
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
+ },
640
617
  },
641
618
  );
642
619
  ```
643
620
 
644
- ### Core handleCallback
621
+ ### `handleNodeCallback(handler, options?)`
645
622
 
646
- The core `handleCallback` is an async function that takes already-parsed request data. Use it to build custom framework integrations:
623
+ Vercel only. Returns `(req, res) => Promise<void>` for frameworks that export Connect-style handlers.
647
624
 
648
625
  ```typescript
649
- import { handleCallback, parseCallback, parseRawCallback } from "@vercel/queue";
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
+ ```
650
636
 
651
- // Web API Request
652
- const parsed = await parseCallback(request);
637
+ ### Handler Signature
653
638
 
654
- // Or, for frameworks that pre-parse the body (e.g. Pages Router)
655
- const parsed = parseRawCallback(req.body, req.headers);
639
+ ```typescript
640
+ type MessageHandler<T> = (
641
+ message: T,
642
+ metadata: MessageMetadata,
643
+ ) => Promise<void> | void;
656
644
 
657
- try {
658
- await handleCallback(handler, parsed);
659
- } catch (error) {
660
- // handle error → 500
645
+ interface MessageMetadata {
646
+ messageId: string;
647
+ deliveryCount: number;
648
+ createdAt: Date;
649
+ expiresAt?: Date;
650
+ topicName: string;
651
+ consumerGroup: string;
652
+ region: string;
661
653
  }
662
654
  ```
663
655