Tail Workers Gotchas & Debugging
Critical Pitfalls
1. Not Using ctx.waitUntil()
Problem: Async work doesn't complete or tail Worker times out Cause: Handlers exit immediately; awaiting blocks processing Solution:
// ❌ WRONG - fire and forget
export default {
async tail(events) {
fetch(endpoint, { body: JSON.stringify(events) });
}
};
// ❌ WRONG - blocking await
export default {
async tail(events, env, ctx) {
await fetch(endpoint, { body: JSON.stringify(events) });
}
};
// ✅ CORRECT
export default {
async tail(events, env, ctx) {
ctx.waitUntil(
(async () => {
await fetch(endpoint, { body: JSON.stringify(events) });
await processMore();
})()
);
}
};2. Missing tail() Handler
Problem: Producer deployment fails Cause: Worker in tail_consumers doesn't export tail() handler Solution: Ensure export default { async tail(events, env, ctx) { ... } }
3. Outcome vs HTTP Status
Problem: Filtering by wrong status Cause: outcome is script execution status, not HTTP status
// ❌ WRONG
if (event.outcome === 500) { /* never matches */ }
// ✅ CORRECT
if (event.outcome === 'exception') { /* script threw */ }
if (event.event?.response?.status === 500) { /* HTTP 500 */ }4. Timestamp Units
Problem: Dates off by 1000x Cause: Timestamps are epoch milliseconds, not seconds
// ❌ WRONG: const date = new Date(event.eventTimestamp * 1000);
// ✅ CORRECT: const date = new Date(event.eventTimestamp);5. Type Name Mismatch
Problem: Using TailItem type Cause: Old docs used TailItem, SDK uses TraceItem
import type { TraceItem } from '@cloudflare/workers-types';
export default {
async tail(events: TraceItem[], env, ctx) { /* ... */ }
};6. Excessive Logging Volume
Problem: Unexpected high costs Cause: Invoked on EVERY producer request Solution: Sample events
export default {
async tail(events, env, ctx) {
if (Math.random() > 0.1) return; // 10% sample
ctx.waitUntil(sendToEndpoint(events));
}
};7. Serialization Issues
Problem: JSON.stringify() fails Cause: log.message is unknown[] with non-serializable values Solution:
const safePayload = events.map(e => ({
...e,
logs: e.logs.map(log => ({
...log,
message: log.message.map(m => {
try { return JSON.parse(JSON.stringify(m)); }
catch { return String(m); }
})
}))
}));8. Missing Error Handling
Problem: Tail Worker silently fails Cause: No try/catch Solution:
ctx.waitUntil((async () => {
try {
await fetch(env.ENDPOINT, { body: JSON.stringify(events) });
} catch (error) {
console.error("Tail error:", error);
await env.FALLBACK_KV.put(`failed:${Date.now()}`, JSON.stringify(events));
}
})());9. Deployment Order
Problem: Producer deployment fails Cause: Tail consumer not deployed yet Solution: Deploy tail consumer FIRST
cd tail-worker && wrangler deploy
cd ../producer && wrangler deploy10. No Event Retry
Problem: Events lost when handler fails Cause: Failed invocations NOT retried Solution: Implement fallback storage (see #8)
Debugging
View logs: wrangler tail my-tail-worker
Incremental testing:
- Verify receipt:
console.log('Events:', events.length) - Inspect structure:
console.log(JSON.stringify(events[0], null, 2)) - Add external call with
ctx.waitUntil()
Monitor dashboard: Check invocation count (matches producer?), error rate, CPU time
Testing
Add test endpoint to producer:
export default {
async fetch(request) {
if (request.url.includes('/test')) {
console.log('Test log');
throw new Error('Test error');
}
return new Response('OK');
}
};Trigger: curl https://producer.example.workers.dev/test
Common Errors
| Error | Cause | Solution |
|---|---|---|
| "Tail consumer not found" | Not deployed | Deploy tail Worker first |
| "No tail handler" | Missing tail() | Add to default export |
| "waitUntil is not a function" | Missing ctx | Add ctx parameter |
| Timeout | Blocking await | Use ctx.waitUntil() |
Performance Notes
- Max 100 events per invocation
- Each consumer receives all events independently
- CPU limits same as regular Workers
- For high volume, use Durable Objects batching