Chapter 04 · Cloudflare Deploy
Subchapter 4.221
references/tail-workers/gotchas.mdMarkdown4 KBView on GitHub
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();
})()
);
}
};Problem: Producer deployment fails
Cause: Worker in tail_consumers doesn’t export tail() handler
Solution: Ensure export default { async tail(events, env, ctx) { ... } }
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 */ }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);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) { /* ... */ }
};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));
}
};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); }
})
}))
}));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));
}
})());Problem: Producer deployment fails
Cause: Tail consumer not deployed yet
Solution: Deploy tail consumer FIRST
cd tail-worker && wrangler deploy
cd ../producer && wrangler deployProblem: Events lost when handler fails
Cause: Failed invocations NOT retried
Solution: Implement fallback storage (see #8)
View logs: wrangler tail my-tail-worker
Incremental testing:
console.log('Events:', events.length)console.log(JSON.stringify(events[0], null, 2))ctx.waitUntil()Monitor dashboard: Check invocation count (matches producer?), error rate, CPU time
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
| 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() |