@vercel/queue 0.0.0-alpha.3 → 0.0.0-alpha.31

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,16 @@
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
7
+ - **Automatic Queue Triggering**: Vercel automatically triggers your API routes when messages are ready
8
+ - **Next.js Integration**: Built-in support for Next.js API routes and Server Actions
9
+ - **Generic Payload Support**: Send and receive any type of data with type safety
15
10
  - **Pub/Sub Pattern**: Topic-based messaging with consumer groups
16
11
  - **Type Safety**: Full TypeScript support with generic types
17
- - **Automatic Retries**: Built-in visibility timeout management
12
+ - **Streaming Support**: Handle large payloads efficiently
13
+ - **Customizable Serialization**: Use built-in transports (JSON, Buffer, Stream) or create your own
18
14
 
19
15
  ## Installation
20
16
 
@@ -22,930 +18,422 @@ with customizable serialization/deserialization (transport) support, including
22
18
  npm install @vercel/queue
23
19
  ```
24
20
 
21
+ The package includes:
22
+
23
+ - **Main Library**: Queue client and utilities for production and development
24
+ - **CLI Tool**: `npx vercel-queue-local-init` for local development handler initialization
25
+
25
26
  ## Quick Start
26
27
 
27
- For local development, you'll need to pull your Vercel environment variables
28
- (including the OIDC token):
28
+ For local development, you'll need to set up your Vercel project:
29
29
 
30
30
  ```bash
31
31
  # Install Vercel CLI if you haven't already
32
32
  npm i -g vercel
33
33
 
34
+ # Link your project to Vercel
35
+ vc link
36
+
34
37
  # Pull environment variables from your Vercel project
35
38
  vc env pull
36
39
  ```
37
40
 
38
- Publishing and consuming messages on a queue
41
+ ## Local Development
39
42
 
40
- ```typescript
41
- // index.ts
42
- import { createTopic, JsonTransport, QueueClient } from "@vercel/queue";
43
+ **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.
43
44
 
44
- // Create a client - automatically authenticated using the OIDC token
45
- const client = await QueueClient.fromVercelFunction();
45
+ ### Next.js Lazy Loading
46
46
 
47
- // Create a topic with JSON serialization (default)
48
- const topic = createTopic<{ message: string; timestamp: number }>(
49
- client,
50
- "my-topic",
51
- );
47
+ For Next.js API routes (or others that are lazy-loaded), run this simple command to initialize handlers:
52
48
 
53
- // Publish a message
54
- await topic.publish({
55
- message: "Hello, World!",
56
- timestamp: Date.now(),
57
- });
49
+ ```bash
50
+ npx vercel-queue-local-init
51
+ ```
58
52
 
59
- // Create a consumer group
60
- const consumer = topic.consumerGroup("my-processors");
53
+ That's it! The script reads your `vercel.json`, finds your queue handlers, and triggers Next.js to load them.
61
54
 
62
- // Process messages continuously with cancellation support
63
- const controller = new AbortController();
55
+ ### Example Workflow
64
56
 
65
- // Start processing (blocks until aborted or error)
66
- try {
67
- await consumer.subscribe(controller.signal, async (message) => {
68
- console.log("Received:", message.payload.message);
69
- console.log("Timestamp:", new Date(message.payload.timestamp));
70
- });
71
- } catch (error) {
72
- console.error("Processing stopped due to error:", error);
73
- }
74
-
75
- // Stop processing from elsewhere in your code
76
- // controller.abort();
77
- ```
57
+ ```bash
58
+ # Start your dev server
59
+ npm run dev
78
60
 
79
- Run the script
61
+ # Initialize handlers (only needed for frameworks that lazy load routes in dev)
62
+ npx vercel-queue-local-init
80
63
 
81
- ```bash
82
- # Using dotenv to load the OIDC token
83
- dotenv -e .env.local node index.ts
64
+ # Send messages - they process locally automatically!
84
65
  ```
85
66
 
86
- ## Usage with Vercel
67
+ ### CLI Options
87
68
 
88
- When deploying on Vercel, rather than having a persistent server subscribed to a
89
- queue, Vercel can trigger a callback route when a message is ready for
90
- consumption.
69
+ ```bash
70
+ # Custom port
71
+ npx vercel-queue-local-init --port 3001
91
72
 
92
- To demonstrate using queues on Vercel, let's use a Next.js app. You can use an
93
- existing app or create one using
94
- [create-next-app](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
73
+ # Different config file
74
+ npx vercel-queue-local-init --config ./my-vercel.json
75
+ ```
95
76
 
96
77
  ### TypeScript Configuration
97
78
 
98
- Update your `tsconfig.json` to use `"bundler"` module resolution for proper
99
- package export resolution:
79
+ Update your `tsconfig.json` to use `"bundler"` module resolution for proper package export resolution:
100
80
 
101
81
  ```json
102
82
  {
103
83
  "compilerOptions": {
104
84
  "moduleResolution": "bundler"
105
- // ... other options
106
85
  }
107
86
  }
108
87
  ```
109
88
 
110
- ### Publishing messages to a queue
89
+ ### Publishing Messages
111
90
 
112
- Create a new server function to publish messages
91
+ The `send` function can be used anywhere in your codebase to publish messages to a queue:
113
92
 
114
93
  ```typescript
115
- // app/action.ts
116
- "use server";
117
-
118
- import { createTopic, QueueClient } from "@vercel/queue";
119
-
120
- export async function publishTestMessage(message: string) {
121
- // Initialize a queue client
122
- const client = await QueueClient.fromVercelFunction();
123
-
124
- // Create a topic with JSON serialization (default)
125
- const topic = createTopic<{ message: string; timestamp: number }>(
126
- client,
127
- "my-topic",
128
- );
129
-
130
- // Publish the message
131
- const { messageId } = await topic.publish(
132
- { message, timestamp: Date.now() },
133
- {
134
- // Provide a callback URL to invoke a consumer when the message is ready to be processed
135
- callback: {
136
- url: process.env.VERCEL_PROJECT_PRODUCTION_URL
137
- ? `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}/api/queue/handle`
138
- : "http://localhost:3000/api/queue/handle",
139
- },
140
- },
141
- );
142
-
143
- console.log(`Published message ${messageId}`);
144
- }
145
- ```
146
-
147
- Now wire up the server function to your app
148
-
149
- ```jsx
150
- // app/some/page.tsx
151
- "use client";
152
- import { publishTestMessage } from "./actions";
153
-
154
- export default function Button() {
155
- return (
156
- // ...
157
- <Button onClick={() => publishTestMessage("Hello world")} >
158
- Publish Test Message
159
- </a>
160
- );
161
- }
162
- ```
94
+ import { send } from "@vercel/queue";
163
95
 
164
- ### Consuming the queue
165
-
166
- Instead of running a persistent server that subscribes to the queue, we use the
167
- callback functionality of Vercel queues to consume messages on the fly, when a
168
- message is ready to be processed.
169
-
170
- The `handleCallback` helper function simplifies queue callback handling in NextJS:
171
-
172
- ```typescript
173
- // app/api/queue/handle/route.ts
174
- import { handleCallback } from "@vercel/queue";
175
-
176
- export const POST = handleCallback({
177
- // Handle messages sent on the "new-users" topic (the consumer
178
- // group "default" will be used)
179
- "my-topic": (message, metadata) => {
180
- console.log(`Received message:`, message, metadata);
181
- // metadata: { messageId, deliveryCount, timestamp }
182
- },
96
+ // Send a message to a topic
97
+ await send("my-topic", {
98
+ message: "Hello world",
183
99
  });
184
100
 
185
- // Or, specify separate handlers for separate consumer groups
186
- export const POST = handleCallback({
187
- // topic: "my-topic"
188
- "my-topic": {
189
- // consumer group: "compress"
190
- compress: (message, metadata) => {
191
- console.log("Compressing image:", message);
192
- },
193
- // consumer group: "resize"
194
- resize: (message, metadata) => {
195
- console.log("Resizing image:", message);
196
- },
197
- // consumer group: "watermark"
198
- watermark: (message, metadata) => {
199
- console.log("Adding watermark:", message);
200
- },
101
+ // With additional options
102
+ await send(
103
+ "my-topic",
104
+ {
105
+ message: "Hello world",
106
+ },
107
+ {
108
+ idempotencyKey: "unique-key", // Optional: prevent duplicate messages
109
+ retentionSeconds: 3600, // Optional: override retention time (defaults to 24 hours)
201
110
  },
202
- });
203
- ```
204
-
205
- ## Key Features
206
-
207
- ### Streaming Support
208
-
209
- Handle large files and data streams without loading them into memory:
210
-
211
- ```typescript
212
- import { StreamTransport } from "@vercel/queue";
213
-
214
- const videoTopic = createTopic<ReadableStream<Uint8Array>>(
215
- client,
216
- "video-processing",
217
- new StreamTransport(),
218
111
  );
219
-
220
- // Process large video files efficiently
221
- const processor = videoTopic.consumerGroup("processors");
222
- await processor.subscribe(signal, async (message) => {
223
- const videoStream = message.payload;
224
- // Process stream chunk by chunk
225
- const reader = videoStream.getReader();
226
- while (true) {
227
- const { done, value } = await reader.read();
228
- if (done) break;
229
- await processChunk(value);
230
- }
231
- });
232
112
  ```
233
113
 
234
- ### Consumer Groups
235
-
236
- Multiple consumers can process messages from the same topic in parallel:
114
+ Example usage in an API route:
237
115
 
238
116
  ```typescript
239
- // Multiple workers in the same group - they share/split messages
240
- const worker1 = topic.consumerGroup("workers");
241
- const worker2 = topic.consumerGroup("workers"); // Same group name
242
- // worker1 and worker2 will receive different messages (load balancing)
243
-
244
- // Different consumer groups - each gets copies of ALL messages
245
- const analytics = topic.consumerGroup("analytics");
246
- const webhooks = topic.consumerGroup("webhooks");
247
- // analytics and webhooks will both receive every message
248
- ```
117
+ // app/api/send-message/route.ts
118
+ import { send } from "@vercel/queue";
249
119
 
250
- ## Architecture
120
+ export async function POST(request: Request) {
121
+ const body = await request.json();
251
122
 
252
- - **Topics**: Named message channels with configurable serialization
253
- - **Consumer Groups**: Named groups of consumers that process messages in
254
- parallel
255
- - `subscribe()`: Continuously process messages with automatic polling
256
- - `receiveMessage()`: Process a specific message by ID
257
- - `receiveNextMessage()`: Process the next available message (one-shot)
258
- - `handleMessage()`: Process message metadata only (without payload)
259
- - **Transports**: Pluggable serialization/deserialization for different data
260
- types
261
- - **Streaming**: Memory-efficient processing of large payloads
262
- - **Visibility Timeouts**: Automatic message lifecycle management
263
-
264
- ## Performance
265
-
266
- The multipart parser is optimized for high-throughput scenarios:
267
-
268
- - **Streaming**: Messages are yielded immediately as headers are parsed
269
- - **Memory Efficient**: No buffering of complete payloads
270
- - **Fast Parsing**: Native Buffer operations for ~50% performance improvement
271
- - **Scalable**: Can handle arbitrarily large responses without memory
272
- constraints
273
-
274
- ## Serialization (Transport) System
275
-
276
- The queue client supports customizable serialization through the `Transport`
277
- interface with **streaming support** for memory-efficient processing. Transport
278
- can be configured at the **topic level** when creating a topic, or at the
279
- **consumer group level** when creating a consumer group.
280
-
281
- ### Built-in Transports
282
-
283
- #### JsonTransport (Default)
284
-
285
- Buffers data for JSON parsing - suitable for structured data that fits in
286
- memory.
287
-
288
- ```typescript
289
- import { createTopic, JsonTransport } from "@vercel/queue";
123
+ const { messageId } = await send("my-topic", {
124
+ message: body.message,
125
+ });
290
126
 
291
- const topic = createTopic<{ data: any }>(
292
- client,
293
- "json-topic",
294
- new JsonTransport(),
295
- );
296
- // or simply (JsonTransport is the default):
297
- const topic = createTopic<{ data: any }>(client, "json-topic");
127
+ return Response.json({ messageId });
128
+ }
298
129
  ```
299
130
 
300
- #### BufferTransport
131
+ ### Consuming Messages
301
132
 
302
- Buffers the entire payload into memory as a Buffer - suitable for binary data
303
- that fits in memory.
133
+ Messages are consumed using API routes that Vercel automatically triggers when messages are available.
304
134
 
305
- ```typescript
306
- import { BufferTransport, createTopic } from "@vercel/queue";
307
-
308
- const topic = createTopic<Buffer>(
309
- client,
310
- "binary-topic",
311
- new BufferTransport(),
312
- );
313
- const binaryData = Buffer.from("Binary data", "utf8");
314
- await topic.publish(binaryData);
315
- ```
135
+ #### 1. Create API Routes
316
136
 
317
- #### StreamTransport
137
+ ##### App Router (Recommended)
318
138
 
319
- **True streaming support** - passes ReadableStream directly without buffering.
320
- Ideal for large files and memory-efficient processing.
139
+ The recommended approach is to handle multiple topics and consumers in a single API route to keep your `vercel.json` configuration simple:
321
140
 
322
141
  ```typescript
323
- import { createTopic, StreamTransport } from "@vercel/queue";
142
+ // app/api/queue/route.ts
143
+ import { handleCallback } from "@vercel/queue";
324
144
 
325
- const topic = createTopic<ReadableStream<Uint8Array>>(
326
- client,
327
- "streaming-topic",
328
- new StreamTransport(),
329
- );
145
+ export const POST = handleCallback({
146
+ // Single topic with one consumer
147
+ "my-topic": {
148
+ "my-consumer": async (message, metadata) => {
149
+ // metadata includes: { messageId, deliveryCount, createdAt }
150
+ console.log("Processing message:", message);
330
151
 
331
- // Send large file as stream without loading into memory
332
- const fileStream = new ReadableStream<Uint8Array>({
333
- start(controller) {
334
- // Read file in chunks
335
- for (const chunk of readFileInChunks("large-file.bin")) {
336
- controller.enqueue(chunk);
337
- }
338
- controller.close();
152
+ // If this throws an error, the message will be automatically retried
153
+ await processMessage(message);
154
+ },
339
155
  },
340
- });
341
-
342
- await topic.publish(fileStream);
343
- ```
344
-
345
- ### Custom Transport
346
-
347
- You can create your own serialization format by implementing the `Transport`
348
- interface:
349
-
350
- ```typescript
351
- import { Transport } from "@vercel/queue";
352
-
353
- interface Transport<T = unknown> {
354
- serialize(value: T): Buffer | ReadableStream<Uint8Array>;
355
- deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
356
- contentType: string;
357
- }
358
- ```
359
-
360
- ### Choosing the Right Transport
361
-
362
- | Use Case | Recommended Transport | Memory Usage | Performance |
363
- | ---------------------- | --------------------- | ------------ | ----------- |
364
- | Small JSON objects | `JsonTransport` | Low | High |
365
- | Binary files < 100MB | `BufferTransport` | Medium | High |
366
- | Large files > 100MB | `StreamTransport` | Very Low | Medium |
367
- | Real-time data streams | `StreamTransport` | Very Low | High |
368
- | Custom protocols | Custom implementation | Varies | Varies |
369
-
370
- ## API Reference
371
156
 
372
- ### QueueClient
373
-
374
- ```typescript
375
- const client = new QueueClient({
376
- token: string;
377
- baseUrl?: string; // defaults to 'https://vqs.vercel.sh'
378
- });
379
- ```
380
-
381
- ### Topic
382
-
383
- ```typescript
384
- const topic = createTopic<T>(client, topicName, transport?);
385
-
386
- // Publish a message (uses topic's transport)
387
- await topic.publish(payload, options?);
388
-
389
- // Trigger a callback URL when the message is
390
- // ready for consumption
391
- await topic.publish(payload, {
392
- callback: { url: "https://example.com/webhook" }
393
- });
157
+ // Multiple consumers for different purposes
158
+ "order-events": {
159
+ fulfillment: async (order, metadata) => {
160
+ // By default, errors will trigger automatic retries
161
+ // But you can control retry timing if needed:
162
+ if (!isSystemReady()) {
163
+ // Override default retry with a 5 minute delay
164
+ return { timeoutSeconds: 300 };
165
+ }
394
166
 
395
- // Or provide multiple callbacks (each URL is called
396
- // with a separate consumer group)
397
- await topic.publish(payload, {
398
- callback: {
399
- group1: { url: "https://example.com/webhook1" },
400
- group2: { url: "https://example.com/webhook2", delay: 30 }
401
- }
167
+ await processOrder(order);
168
+ },
169
+ analytics: async (order, metadata) => {
170
+ try {
171
+ await trackOrder(order);
172
+ } catch (error) {
173
+ // Optional: Custom exponential backoff instead of default retry timing
174
+ const timeoutSeconds = Math.pow(2, metadata.deliveryCount) * 60;
175
+ return { timeoutSeconds };
176
+ }
177
+ },
178
+ },
402
179
  });
403
-
404
- // Create a consumer group (can override transport)
405
- const consumer = topic.consumerGroup<U>(groupName, options?);
406
- ```
407
-
408
- ### ConsumerGroup
409
-
410
- ```typescript
411
- // Start continuous processing (blocks until signal is aborted or error occurs)
412
- await consumer.subscribe(signal, handler, options?);
413
-
414
- // Process a specific message by ID
415
- await consumer.receiveMessage(messageId, handler);
416
-
417
- // Process the next available message
418
- await consumer.receiveNextMessage(handler);
419
-
420
- // Handle a specific message by ID without payload
421
- await consumer.handleMessage(messageId, handler);
422
180
  ```
423
181
 
424
- ### Message Handler
182
+ While you can split handlers into separate routes if needed (e.g., for code organization or deployment flexibility), consolidating them in one route is recommended for simpler configuration.
425
183
 
426
- ```typescript
427
- // Handler function signature
428
- type MessageHandler<T> = (
429
- message: Message<T>,
430
- ) => Promise<MessageHandlerResult> | MessageHandlerResult;
184
+ ##### Pages Router
431
185
 
432
- // Handler result types
433
- type MessageHandlerResult = void | MessageTimeoutResult;
434
-
435
- interface MessageTimeoutResult {
436
- timeoutSeconds: number; // seconds before message becomes available again
437
- }
438
- ```
439
-
440
- ### Transport Interface
186
+ For Next.js Pages Router, import from `@vercel/queue/nextjs/pages` to get a handler compatible with the Pages Router API (`NextApiRequest`/`NextApiResponse`):
441
187
 
442
188
  ```typescript
443
- interface Transport<T = unknown> {
444
- serialize(value: T): Buffer | ReadableStream<Uint8Array>;
445
- deserialize(stream: ReadableStream<Uint8Array>): Promise<T>;
446
- contentType: string;
447
- }
448
- ```
189
+ // pages/api/queue.ts
190
+ import { handleCallback } from "@vercel/queue/nextjs/pages";
449
191
 
450
- ### Callback Utilities
451
-
452
- ```typescript
453
- // Parse queue callback request headers
454
- function parseCallbackRequest(request: Request): CallbackMessageOptions;
455
-
456
- // Callback options type
457
- interface CallbackMessageOptions {
458
- queueName: string;
459
- consumerGroup: string;
460
- messageId: string;
461
- }
462
- // Create a callback handler for NextJS route handlers
463
- function handleCallback(handlers: CallbackHandlers): (request: Request) => Promise<Response>;
464
-
465
- // Handler function signature for callbacks
466
- type Handler<T = unknown> = (
467
- payload: T,
468
- metadata: MessageMetadata
469
- ) => Promise<MessageHandlerResult> | MessageHandlerResult;
470
-
471
- // Message metadata provided to handlers
472
- interface MessageMetadata {
473
- messageId: string;
474
- deliveryCount: number;
475
- timestamp: string;
476
- }
477
-
478
- // Configuration object with handlers for different topics
479
- type CallbackHandlers = {
480
- [topicName: string]:
481
- | Handler // Single handler (uses 'default' consumer group)
482
- | { [consumerGroup: string]: Handler }; // Multiple consumer group handlers
483
- };
484
-
485
- // Example usage:
486
- export const POST = handleCallback({
487
- // Topic handler (uses 'default' consumer group)
488
- "new-users": (message, metadata) => {
489
- console.log(`New user event:`, message, metadata);
192
+ export default handleCallback({
193
+ "my-topic": {
194
+ "my-consumer": async (message, metadata) => {
195
+ console.log("Processing message:", message);
196
+ await processMessage(message);
197
+ },
198
+ },
199
+ "order-events": {
200
+ fulfillment: async (order, metadata) => {
201
+ if (!isSystemReady()) {
202
+ return { timeoutSeconds: 300 };
203
+ }
204
+ await processOrder(order);
205
+ },
206
+ analytics: async (order, metadata) => {
207
+ await trackOrder(order);
208
+ },
490
209
  },
491
-
492
- // Consumer group specific handlers
493
- "image-processing": {
494
- "compress": (message, metadata) => console.log("Compressing image", message),
495
- "resize": (message, metadata) => console.log("Resizing image", message),
496
- }
497
210
  });
498
-
499
- // Error thrown for invalid callback requests
500
- class InvalidCallbackError extends Error;
501
211
  ```
502
212
 
503
- ## Examples
504
-
505
- ### Basic JSON Processing
506
-
507
- ```typescript
508
- interface UserEvent {
509
- userId: string;
510
- action: string;
511
- timestamp: number;
512
- }
513
-
514
- const userTopic = createTopic<UserEvent>(client, "user-events");
213
+ The `/nextjs/pages` subpath export automatically adapts the handler to work with the Pages Router API.
515
214
 
516
- await userTopic.publish({
517
- userId: "123",
518
- action: "login",
519
- timestamp: Date.now(),
520
- });
215
+ #### 2. Configure vercel.json
521
216
 
522
- const consumer = userTopic.consumerGroup("processors");
523
- const controller = new AbortController();
524
-
525
- try {
526
- await consumer.subscribe(controller.signal, async (message) => {
527
- console.log(
528
- `User ${message.payload.userId} performed ${message.payload.action}`,
529
- );
530
- });
531
- } catch (error) {
532
- console.error("Processing error:", error);
533
- }
534
-
535
- // Stop processing when needed
536
- // controller.abort();
537
- ```
538
-
539
- ### Processing Specific Messages by ID
540
-
541
- ```typescript
542
- const userTopic = createTopic<{ userId: string; action: string }>(
543
- client,
544
- "user-events",
545
- );
546
- const consumer = userTopic.consumerGroup("processors");
217
+ Configure which topics and consumers your API route handles.
547
218
 
548
- // Process a specific message if you know its ID
549
- const messageId = "01234567-89ab-cdef-0123-456789abcdef";
219
+ **For App Router:**
550
220
 
551
- try {
552
- await consumer.receiveMessage(messageId, async (message) => {
553
- console.log(`Processing specific message: ${message.messageId}`);
554
- console.log(
555
- `User ${message.payload.userId} performed ${message.payload.action}`,
556
- );
557
- });
558
- console.log("Message processed successfully");
559
- } catch (error) {
560
- if (error.message.includes("not found or not available")) {
561
- console.log("Message was already processed or does not exist");
562
- } else if (error.message.includes("FIFO ordering violation")) {
563
- console.log("FIFO queue requires processing messages in order");
564
- } else {
565
- console.error("Error processing message:", error);
221
+ ```json
222
+ {
223
+ "functions": {
224
+ "app/api/queue/route.ts": {
225
+ "experimentalTriggers": [
226
+ {
227
+ "type": "queue/v1beta",
228
+ "topic": "my-topic",
229
+ "consumer": "my-consumer",
230
+ "retryAfterSeconds": 60,
231
+ "initialDelaySeconds": 0
232
+ },
233
+ {
234
+ "type": "queue/v1beta",
235
+ "topic": "order-events",
236
+ "consumer": "fulfillment"
237
+ },
238
+ {
239
+ "type": "queue/v1beta",
240
+ "topic": "order-events",
241
+ "consumer": "analytics",
242
+ "retryAfterSeconds": 300
243
+ }
244
+ ]
245
+ }
566
246
  }
567
247
  }
568
248
  ```
569
249
 
570
- ### Processing Next Available Message
571
-
572
- ```typescript
573
- const workTopic = createTopic<{ taskType: string; data: any }>(
574
- client,
575
- "work-queue",
576
- );
577
- const worker = workTopic.consumerGroup("workers");
250
+ **For Pages Router:**
578
251
 
579
- // Process the next available message (one-shot processing)
580
- try {
581
- await worker.receiveNextMessage(async (message) => {
582
- console.log(`Processing task: ${message.payload.taskType}`);
583
- await processTask(message.payload.taskType, message.payload.data);
584
- });
585
- console.log("Message processed successfully");
586
- } catch (error) {
587
- if (error instanceof QueueEmptyError) {
588
- console.log("No messages available");
589
- } else if (error instanceof MessageLockedError) {
590
- console.log("Next message is locked (FIFO queue)");
591
- if (error.retryAfter) {
592
- console.log(`Retry after ${error.retryAfter} seconds`);
252
+ ```json
253
+ {
254
+ "functions": {
255
+ "pages/api/queue.ts": {
256
+ "experimentalTriggers": [
257
+ {
258
+ "type": "queue/v1beta",
259
+ "topic": "my-topic",
260
+ "consumer": "my-consumer",
261
+ "retryAfterSeconds": 60,
262
+ "initialDelaySeconds": 0
263
+ },
264
+ {
265
+ "type": "queue/v1beta",
266
+ "topic": "order-events",
267
+ "consumer": "fulfillment"
268
+ },
269
+ {
270
+ "type": "queue/v1beta",
271
+ "topic": "order-events",
272
+ "consumer": "analytics",
273
+ "retryAfterSeconds": 300
274
+ }
275
+ ]
593
276
  }
594
- } else {
595
- console.error("Error processing message:", error);
596
277
  }
597
278
  }
598
-
599
- // You can also use it with timeout results
600
- await worker.receiveNextMessage(async (message) => {
601
- if (!canProcessTaskType(message.payload.taskType)) {
602
- // Return timeout to retry later
603
- return { timeoutSeconds: 60 };
604
- }
605
-
606
- await processTask(message.payload.taskType, message.payload.data);
607
- });
608
279
  ```
609
280
 
610
- ### Timing Out Messages
281
+ ### Key Concepts
611
282
 
612
- ```typescript
613
- const workTopic = createTopic<{ taskType: string; data: any }>(
614
- client,
615
- "work-queue",
616
- );
617
- const worker = workTopic.consumerGroup("workers");
618
- const controller = new AbortController();
283
+ - **Topics**: Named message channels that can have multiple consumer groups
284
+ - **Consumer Groups**: Named groups of consumers that process messages in parallel
285
+ - Different consumer groups for the same topic each get a copy of every message
286
+ - Multiple consumers in the same group share/split messages for load balancing
287
+ - **Automatic Triggering**: Vercel triggers your API routes when messages are available
288
+ - **Message Processing**: Your API routes receive message metadata via headers
289
+ - **Configuration**: The `vercel.json` file tells Vercel which routes handle which topics/consumers
619
290
 
620
- try {
621
- await worker.subscribe(controller.signal, async (message) => {
622
- const { taskType, data } = message.payload;
291
+ ## Advanced Features
623
292
 
624
- // Check if we can process this task type right now
625
- if (taskType === "heavy-computation" && isSystemOverloaded()) {
626
- // Return timeout to retry later (5 minutes)
627
- return { timeoutSeconds: 300 };
628
- }
293
+ ### Serialization (Transport) System
629
294
 
630
- // Check if we have required resources
631
- if (taskType === "external-api" && !isExternalServiceAvailable()) {
632
- // Return timeout to retry in 1 minute
633
- return { timeoutSeconds: 60 };
634
- }
295
+ The queue client supports customizable serialization through the `Transport` interface:
635
296
 
636
- // Process the message normally
637
- console.log(`Processing ${taskType} task`);
638
- await processTask(taskType, data);
639
- // Message will be automatically deleted on successful completion
640
- });
641
- } catch (error) {
642
- console.error("Worker processing error:", error);
643
- }
297
+ #### Built-in Transports
644
298
 
645
- // Example with exponential backoff
646
- const backoffController = new AbortController();
299
+ 1. **JsonTransport (Default)**: For structured data that fits in memory
300
+ 2. **BufferTransport**: For binary data that fits in memory
301
+ 3. **StreamTransport**: For large files and memory-efficient processing
647
302
 
648
- try {
649
- await worker.subscribe(backoffController.signal, async (message) => {
650
- const maxRetries = 3;
651
- const deliveryCount = message.deliveryCount;
652
-
653
- try {
654
- await processMessage(message.payload);
655
- // Successful processing - message will be deleted
656
- } catch (error) {
657
- if (deliveryCount < maxRetries) {
658
- // Exponential backoff: 2^deliveryCount minutes
659
- const timeoutSeconds = Math.pow(2, deliveryCount) * 60;
660
- console.log(
661
- `Retrying message in ${timeoutSeconds} seconds (attempt ${deliveryCount})`,
662
- );
663
- return { timeoutSeconds: timeoutSeconds };
664
- } else {
665
- // Max retries reached, let the message fail and be deleted
666
- console.error("Max retries reached, message will be discarded:", error);
667
- throw error;
668
- }
669
- }
670
- });
671
- } catch (error) {
672
- console.error("Backoff processing error:", error);
673
- }
674
- ```
675
-
676
- ### Complete Example: Video Processing Pipeline
677
-
678
- Here's a comprehensive example showing a video processing pipeline that
679
- processes videos with FFmpeg and stores the results in Vercel Blob:
303
+ Example:
680
304
 
681
305
  ```typescript
682
- import { createTopic, QueueClient, StreamTransport } from "@vercel/queue";
683
- import { spawn } from "child_process";
684
- import ffmpeg from "ffmpeg-static";
685
- import { put } from "@vercel/blob";
306
+ import { send, JsonTransport } from "@vercel/queue";
686
307
 
687
- const client = new QueueClient({
688
- token: "your-vercel-oidc-token",
689
- });
690
-
691
- // Input topic with unoptimized videos
692
- const unoptimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
693
- client,
694
- "unoptimized-videos",
695
- new StreamTransport(),
696
- );
308
+ // JsonTransport is the default
309
+ await send("json-topic", { data: "example" });
697
310
 
698
- // Output topic for optimized videos
699
- const optimizedVideosTopic = createTopic<ReadableStream<Uint8Array>>(
700
- client,
701
- "optimized-videos",
702
- new StreamTransport(),
311
+ // Explicit transport configuration
312
+ await send(
313
+ "json-topic",
314
+ { data: "example" },
315
+ { transport: new JsonTransport() },
703
316
  );
704
-
705
- // Step 1: Process videos with FFmpeg
706
- const videoProcessor = unoptimizedVideosTopic.consumerGroup("processors");
707
- const processingController = new AbortController();
708
-
709
- try {
710
- await videoProcessor.subscribe(
711
- processingController.signal,
712
- async (message) => {
713
- const inputVideoStream = message.payload;
714
- console.log("Processing video...");
715
-
716
- if (!ffmpeg) {
717
- throw new Error("FFmpeg not available");
718
- }
719
-
720
- // Create optimized video stream using FFmpeg
721
- const optimizedStream = new ReadableStream<Uint8Array>({
722
- start(controller) {
723
- const ffmpegProcess = spawn(
724
- ffmpeg,
725
- [
726
- "-i",
727
- "pipe:0", // Input from stdin
728
- "-c:v",
729
- "libvpx-vp9", // Video codec
730
- "-c:a",
731
- "libopus", // Audio codec
732
- "-crf",
733
- "23", // Quality
734
- "-f",
735
- "webm", // Output format
736
- "pipe:1", // Output to stdout
737
- ],
738
- { stdio: ["pipe", "pipe", "pipe"] },
739
- );
740
-
741
- // Pipe input stream to FFmpeg
742
- const reader = inputVideoStream.getReader();
743
- const pipeInput = async () => {
744
- while (true) {
745
- const { done, value } = await reader.read();
746
- if (done) {
747
- ffmpegProcess.stdin?.end();
748
- break;
749
- }
750
- ffmpegProcess.stdin?.write(value);
751
- }
752
- };
753
- pipeInput();
754
-
755
- // Stream FFmpeg output
756
- ffmpegProcess.stdout?.on("data", (chunk) => {
757
- controller.enqueue(new Uint8Array(chunk));
758
- });
759
-
760
- ffmpegProcess.on("close", (code) => {
761
- if (code === 0) {
762
- controller.close();
763
- } else {
764
- controller.error(new Error(`FFmpeg failed with code ${code}`));
765
- }
766
- });
767
- },
768
- });
769
-
770
- // Publish optimized video to next topic
771
- await optimizedVideosTopic.publish(optimizedStream);
772
- console.log("Video optimized and published");
773
- },
774
- );
775
- } catch (error) {
776
- console.error("Video processing error:", error);
777
- }
778
-
779
- // Step 2: Store optimized videos in Vercel Blob
780
- const blobUploader = optimizedVideosTopic.consumerGroup("blob-uploaders");
781
- const uploadController = new AbortController();
782
-
783
- try {
784
- await blobUploader.subscribe(uploadController.signal, async (message) => {
785
- const optimizedVideo = message.payload;
786
-
787
- // Upload to Vercel Blob storage
788
- const filename = `optimized-${Date.now()}.webm`;
789
- const blob = await put(filename, optimizedVideo, {
790
- access: "public",
791
- contentType: "video/webm",
792
- });
793
-
794
- console.log(`Video uploaded to blob: ${blob.url} (${blob.size} bytes)`);
795
- });
796
- } catch (error) {
797
- console.error("Blob upload error:", error);
798
- }
799
-
800
- // Graceful shutdown
801
- process.on("SIGINT", () => {
802
- processingController.abort();
803
- uploadController.abort();
804
- });
805
317
  ```
806
318
 
807
- ## Error Handling
808
-
809
- The queue client provides specific error types for different failure scenarios:
810
-
811
- ### Error Types
812
-
813
- - **`QueueEmptyError`**: Thrown when attempting to receive messages from an
814
- empty queue (204 status)
815
-
816
- - Only thrown when directly using `client.receiveMessages()`
817
- - `ConsumerGroup.subscribe()` handles this error internally and continues
818
- polling
819
-
820
- - **`MessageLockedError`**: Thrown when a message is temporarily locked (423
821
- status)
822
-
823
- - Contains optional `retryAfter` property with seconds to wait before retry
824
- - For `receiveMessages()` on FIFO queues: the next message in sequence is
825
- locked
826
- - For `receiveMessageById()`: the requested message is locked
827
- - `ConsumerGroup.subscribe()` handles this error internally when polling
828
-
829
- - **`MessageNotFoundError`**: Message doesn't exist (404 status)
830
-
831
- - **`MessageNotAvailableError`**: Message exists but isn't available for
832
- processing (409 status)
833
-
834
- - **`FifoOrderingViolationError`**: FIFO queue ordering violation (409 status
835
- with nextMessageId)
836
-
837
- - Contains `nextMessageId` property indicating which message to process first
319
+ ### Transport Selection Guide
838
320
 
839
- - **`FailedDependencyError`**: FIFO ordering violation when receiving by ID (424
840
- status)
321
+ | Use Case | Recommended Transport | Memory Usage | Performance |
322
+ | -------------------- | --------------------- | ------------ | ----------- |
323
+ | Small JSON objects | JsonTransport | Low | High |
324
+ | Binary files < 100MB | BufferTransport | Medium | High |
325
+ | Large files > 100MB | StreamTransport | Very Low | Medium |
326
+ | Real-time streams | StreamTransport | Very Low | High |
841
327
 
842
- - Contains `nextMessageId` property indicating which message must be processed
843
- first
844
- - Similar to `FifoOrderingViolationError` but specifically for receive-by-ID
845
- operations
846
-
847
- - **`MessageCorruptedError`**: Message data is corrupted or can't be parsed
848
-
849
- - **`BadRequestError`**: Invalid request parameters (400 status)
850
-
851
- - Invalid queue names, FIFO limit violations, missing required parameters
852
-
853
- - **`UnauthorizedError`**: Authentication failure (401 status)
854
-
855
- - Missing or invalid authentication token
856
-
857
- - **`ForbiddenError`**: Access denied (403 status)
328
+ ## Error Handling
858
329
 
859
- - Queue environment doesn't match token environment
330
+ The queue client provides specific error types:
860
331
 
861
- - **`InternalServerError`**: Server-side errors (500+ status codes)
862
- - Unexpected server errors, service unavailable, etc.
332
+ - **`QueueEmptyError`**: No messages available (204)
333
+ - **`MessageLockedError`**: Message temporarily locked (423)
334
+ - **`MessageNotFoundError`**: Message doesn't exist (404)
335
+ - **`MessageNotAvailableError`**: Message exists but unavailable (409)
336
+ - **`MessageCorruptedError`**: Message data corrupted
337
+ - **`BadRequestError`**: Invalid parameters (400)
338
+ - **`UnauthorizedError`**: Authentication failure (401)
339
+ - **`ForbiddenError`**: Access denied (403)
340
+ - **`InternalServerError`**: Server errors (500+)
863
341
 
864
- ### Error Handling Examples
342
+ Example error handling:
865
343
 
866
344
  ```typescript
867
345
  import {
868
346
  BadRequestError,
869
- FailedDependencyError,
870
- FifoOrderingViolationError,
871
347
  ForbiddenError,
872
348
  InternalServerError,
873
- MessageLockedError,
874
- QueueEmptyError,
875
349
  UnauthorizedError,
876
350
  } from "@vercel/queue";
877
351
 
878
- // Handle empty queue or locked messages
879
- try {
880
- for await (const message of client.receiveMessages(options, transport)) {
881
- // Process messages
882
- }
883
- } catch (error) {
884
- if (error instanceof QueueEmptyError) {
885
- console.log("Queue is empty, retry later");
886
- } else if (error instanceof MessageLockedError) {
887
- console.log("Next message in FIFO queue is locked");
888
- if (error.retryAfter) {
889
- console.log(`Retry after ${error.retryAfter} seconds`);
890
- }
891
- }
892
- }
893
-
894
- // Handle locked message with retry
895
352
  try {
896
- await consumer.receiveMessage(messageId, handler);
897
- } catch (error) {
898
- if (error instanceof MessageLockedError) {
899
- console.log("Message is locked by another consumer");
900
- if (error.retryAfter) {
901
- console.log(`Retry after ${error.retryAfter} seconds`);
902
- setTimeout(() => retry(), error.retryAfter * 1000);
903
- }
904
- } else if (error instanceof FailedDependencyError) {
905
- // FIFO ordering violation for receive by ID
906
- console.log(`Must process ${error.nextMessageId} first`);
907
- }
908
- }
909
-
910
- // Handle authentication and authorization errors
911
- try {
912
- await topic.publish(payload);
353
+ await send("my-topic", payload);
913
354
  } catch (error) {
914
355
  if (error instanceof UnauthorizedError) {
915
356
  console.log("Invalid token - refresh authentication");
916
357
  } else if (error instanceof ForbiddenError) {
917
- console.log("Environment mismatch - check token/queue configuration");
358
+ console.log("Environment mismatch - check configuration");
918
359
  } else if (error instanceof BadRequestError) {
919
360
  console.log("Invalid parameters:", error.message);
920
361
  } else if (error instanceof InternalServerError) {
921
362
  console.log("Server error - retry with backoff");
922
363
  }
923
364
  }
365
+ ```
924
366
 
925
- // Complete error handling pattern
926
- function handleQueueError(error: unknown): void {
927
- if (error instanceof QueueEmptyError || error instanceof MessageLockedError) {
928
- // Transient errors - safe to retry
929
- console.log("Temporary condition, will retry");
930
- } else if (
931
- error instanceof UnauthorizedError ||
932
- error instanceof ForbiddenError
933
- ) {
934
- // Authentication/authorization errors - need to fix configuration
935
- console.log("Auth error - check credentials");
936
- } else if (error instanceof BadRequestError) {
937
- // Client error - fix the request
938
- console.log("Invalid request:", error.message);
939
- } else if (error instanceof InternalServerError) {
940
- // Server error - implement exponential backoff
941
- console.log("Server error - retry with backoff");
942
- } else {
943
- // Unknown error
944
- console.error("Unexpected error:", error);
367
+ ## Advanced Usage
368
+
369
+ ### Direct Message Processing
370
+
371
+ > **Note**: The `receive` function is not intended for use in Vercel deployments. It's designed for use in the Vercel Sandbox environment or alternative server setups where you need direct message processing control.
372
+
373
+ ```typescript
374
+ // Process next available message
375
+ await receive<T>(topicName, consumerGroup, handler);
376
+
377
+ // Process specific message by ID
378
+ await receive<T>(topicName, consumerGroup, handler, {
379
+ messageId: "message-id"
380
+ });
381
+
382
+ // Process message with options
383
+ await receive<T>(topicName, consumerGroup, handler, {
384
+ messageId?: string; // Process specific message by ID
385
+ skipPayload?: boolean; // Skip payload download (requires messageId)
386
+ transport?: Transport<T>; // Custom transport (defaults to JsonTransport)
387
+ visibilityTimeoutSeconds?: number; // Message visibility timeout
388
+ refreshInterval?: number; // Refresh interval for long-running operations
389
+ });
390
+
391
+ // Handler function signature
392
+ type MessageHandler<T = unknown> = (
393
+ message: T,
394
+ metadata: MessageMetadata
395
+ ) => Promise<MessageHandlerResult> | MessageHandlerResult;
396
+
397
+ // Handler result types
398
+ type MessageHandlerResult = void | MessageTimeoutResult;
399
+
400
+ interface MessageTimeoutResult {
401
+ timeoutSeconds: number; // seconds before message becomes available again
402
+ }
403
+ ```
404
+
405
+ ## Limits
406
+
407
+ - **Message Throughput**: Each topic can handle up to 1,000 messages per second
408
+ - **Payload Size**: Maximum payload size is 4.5MB (this limit will be increased soon)
409
+ - **Number of Topics**: No limit on the number of topics you can create
410
+
411
+ ### Scaling Beyond Limits
412
+
413
+ If you need more than 1,000 messages per second, you can create multiple topics (e.g., user-specific or shard-based topics) and handle them with a single consumer using wildcards in your `vercel.json`:
414
+
415
+ ```json
416
+ {
417
+ "functions": {
418
+ "app/api/queue/route.ts": {
419
+ "experimentalTriggers": [
420
+ {
421
+ "type": "queue/v1beta",
422
+ "topic": "user-*",
423
+ "consumer": "processor"
424
+ }
425
+ ]
426
+ }
945
427
  }
946
428
  }
947
429
  ```
948
430
 
431
+ This allows you to:
432
+
433
+ - Create topics like `user-1`, `user-2`, etc.
434
+ - Process messages from all user topics with a single handler
435
+ - Each topic gets its own 1,000 messages per second quota
436
+
949
437
  ## License
950
438
 
951
439
  MIT