@vercel/queue 0.0.0-alpha.2 → 0.0.0-alpha.22

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