# 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:** ```typescript // ❌ 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 ```typescript // ❌ 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 ```typescript // ❌ 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` ```typescript 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 ```typescript 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:** ```typescript 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:** ```typescript 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 ```bash cd tail-worker && wrangler deploy cd ../producer && wrangler deploy ``` ### 10. 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:** 1. Verify receipt: `console.log('Events:', events.length)` 2. Inspect structure: `console.log(JSON.stringify(events[0], null, 2))` 3. Add external call with `ctx.waitUntil()` **Monitor dashboard:** Check invocation count (matches producer?), error rate, CPU time ## Testing Add test endpoint to producer: ```typescript 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