# Queues Gotchas & Troubleshooting ## CRITICAL: Top Production Mistakes ### 1. "Entire Batch Retried After Single Error" **Problem:** Throwing uncaught error in queue handler retries the entire batch, not just the failed message **Cause:** Uncaught exceptions propagate to the runtime, triggering batch-level retry **Solution:** Always wrap individual message processing in try/catch and call `msg.retry()` explicitly ```typescript // ❌ BAD: Throws error, retries entire batch async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { await riskyOperation(msg.body); // If this throws, entire batch retries msg.ack(); } } // ✅ GOOD: Catch per message, handle individually async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { try { await riskyOperation(msg.body); msg.ack(); } catch (error) { msg.retry({ delaySeconds: 60 }); } } } ``` ### 2. "Messages Retry Forever" **Problem:** Messages not explicitly ack'd or retry'd will auto-retry indefinitely **Cause:** Runtime default behavior retries unhandled messages until `max_retries` reached **Solution:** Always call `msg.ack()` or `msg.retry()` for each message. Never leave messages unhandled. ```typescript // ❌ BAD: Skipped messages auto-retry forever async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { if (shouldProcess(msg.body)) { await process(msg.body); msg.ack(); } // Missing: msg.ack() for skipped messages - they will retry! } } // ✅ GOOD: Explicitly handle all messages async queue(batch: MessageBatch): Promise { for (const msg of batch.messages) { if (shouldProcess(msg.body)) { await process(msg.body); msg.ack(); } else { msg.ack(); // Explicitly ack even if not processing } } } ``` ## Common Errors ### "Duplicate Message Processing" **Problem:** Same message processed multiple times **Cause:** At-least-once delivery guarantee means duplicates are possible during retries **Solution:** Design consumers to be idempotent by tracking processed message IDs in KV with expiration TTL ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { const processed = await env.PROCESSED_KV.get(msg.id); if (processed) { msg.ack(); continue; } await processMessage(msg.body); await env.PROCESSED_KV.put(msg.id, '1', { expirationTtl: 86400 }); msg.ack(); } } ``` ### "Pull Consumer Can't Decode Messages" **Problem:** Pull consumer or dashboard shows unreadable message bodies **Cause:** Messages sent with `v8` content type are only decodable by Workers push consumers **Solution:** Use `json` content type for pull consumers or dashboard visibility ```typescript // Use json for pull consumers await env.MY_QUEUE.send(data, { contentType: 'json' }); // Use v8 only for push consumers with complex JS types await env.MY_QUEUE.send({ date: new Date(), tags: new Set() }, { contentType: 'v8' }); ``` ### "Messages Not Being Delivered" **Problem:** Messages sent but consumer not processing **Cause:** Queue paused, consumer not configured, or consumer errors **Solution:** Check queue status with `wrangler queues list`, verify consumer configured with `wrangler queues consumer add`, and check logs with `wrangler tail` ### "High Dead Letter Queue Rate" **Problem:** Many messages ending up in DLQ **Cause:** Consumer repeatedly failing to process messages after max retries **Solution:** Review consumer error logs, check external dependency availability, verify message format matches expectations, or increase retry delay ## Error Classification Patterns Classify errors to decide whether to retry or DLQ: ```typescript async queue(batch: MessageBatch, env: Env): Promise { for (const msg of batch.messages) { try { await processMessage(msg.body); msg.ack(); } catch (error) { // Transient errors: retry with backoff if (isRetryable(error)) { const delay = Math.min(30 * (2 ** msg.attempts), 43200); msg.retry({ delaySeconds: delay }); } // Permanent errors: ack to avoid infinite retries else { console.error('Permanent error, sending to DLQ:', error); await env.ERROR_LOG.put(msg.id, JSON.stringify({ msg: msg.body, error: String(error) })); msg.ack(); // Prevent further retries } } } } function isRetryable(error: unknown): boolean { if (error instanceof Response) { // Retry: rate limits, timeouts, server errors return error.status === 429 || error.status >= 500; } if (error instanceof Error) { // Don't retry: validation, auth, not found return !error.message.includes('validation') && !error.message.includes('unauthorized') && !error.message.includes('not found'); } return false; // Unknown errors don't retry } ``` ### "CPU Time Exceeded in Consumer" **Problem:** Consumer fails with CPU time limit exceeded **Cause:** Consumer processing exceeding 30s default CPU time limit **Solution:** Increase CPU limit in wrangler.jsonc: `{ "limits": { "cpu_ms": 300000 } }` (5 minutes max) ## Content Type Decision Guide **When to use each content type:** | Content Type | Use When | Readable By | Supports | |--------------|----------|-------------|----------| | `json` (default) | Pull consumers, dashboard visibility, simple objects | All (push/pull/dashboard) | JSON-serializable types only | | `v8` | Push consumers only, complex JS objects | Push consumers only | Date, Map, Set, BigInt, typed arrays | | `text` | String-only payloads | All | Strings only | | `bytes` | Binary data (images, files) | All | ArrayBuffer, Uint8Array | **Decision tree:** 1. Need to view in dashboard or use pull consumer? → Use `json` 2. Need Date, Map, Set, or other V8 types? → Use `v8` (push consumers only) 3. Just strings? → Use `text` 4. Binary data? → Use `bytes` ```typescript // Dashboard/pull: use json await env.QUEUE.send({ id: 123, name: 'test' }, { contentType: 'json' }); // Complex JS types (push only): use v8 await env.QUEUE.send({ created: new Date(), tags: new Set(['a', 'b']) }, { contentType: 'v8' }); ``` ## Limits | Limit | Value | Notes | |-------|-------|-------| | Max queues | 10,000 | Per account | | Message size | 128 KB | Maximum per message | | Batch size (consumer) | 100 messages | Maximum messages per batch | | Batch size (sendBatch) | 100 msgs or 256 KB | Whichever limit reached first | | Throughput | 5,000 msgs/sec | Per queue | | Retention | 4-14 days | Configurable retention period | | Max backlog | 25 GB | Maximum queue backlog size | | Max delay | 12 hours (43,200s) | Maximum message delay | | Max retries | 100 | Maximum retry attempts | | CPU time default | 30s | Per consumer invocation | | CPU time max | 300s (5 min) | Configurable via `limits.cpu_ms` | | Operations per message | 3 (write + read + delete) | Base cost per message | | Pricing | $0.40 per 1M operations | After 1M free operations | | Message charging | Per 64 KB chunk | Messages charged in 64 KB increments |