Why can't my tail worker ever complete its job and why does it get terminated by CF quietly?

I have a very simple tail worker, see code below:
export default {
async tail(events: TailEvent[], env: any, ctx: ExecutionContext): Promise<void> {
let sequenceNumber = 0;
const heartbeatInterval = setInterval(() => {
sequenceNumber++;
// I do see first five messages in logs (attached)
console.log(`💓 HEARTBEAT #${sequenceNumber} - Time: ${new Date().toISOString()}`);
}, 5000);

const promise = handleTailWorker(events, env, ctx).finally(() => {
// I do NOT ever see this messgage in the logs
console.log('🛑 Clearing heartbeat timer');
clearInterval(heartbeatInterval);
});

return await promise;
}
};
export default {
async tail(events: TailEvent[], env: any, ctx: ExecutionContext): Promise<void> {
let sequenceNumber = 0;
const heartbeatInterval = setInterval(() => {
sequenceNumber++;
// I do see first five messages in logs (attached)
console.log(`💓 HEARTBEAT #${sequenceNumber} - Time: ${new Date().toISOString()}`);
}, 5000);

const promise = handleTailWorker(events, env, ctx).finally(() => {
// I do NOT ever see this messgage in the logs
console.log('🛑 Clearing heartbeat timer');
clearInterval(heartbeatInterval);
});

return await promise;
}
};
Here's my wrangler.toml:
name = "...-tail"
main = "worker.ts"
compatibility_date = "2024-11-11"
logpush = true

# Routes for tail worker (optional, for health checks)
routes = [
{ pattern = "api....-tail", zone_name = "..." },
]

[observability.logs]
enabled = true

[limits]
cpu_ms = 300_000
name = "...-tail"
main = "worker.ts"
compatibility_date = "2024-11-11"
logpush = true

# Routes for tail worker (optional, for health checks)
routes = [
{ pattern = "api....-tail", zone_name = "..." },
]

[observability.logs]
enabled = true

[limits]
cpu_ms = 300_000
It appears that no matter what tail worker will be terminated after 29,999ms? I could not find any reference to that in the official docs. Perhaps I'm doing something wrong?
No description
No description
4 Replies
Chaika
Chaika3mo ago
This is sort of documented, you only have 30 seconds to do anything after the client disconnects, but should def be more explicit. https://developers.cloudflare.com/workers/platform/limits/#duration Everything that isn't from a client request has an explicit limit on duration, so doubt this will ever be unlimited at least. Maybe the better question is, what are you trying to do that is impacted by this?
useapi.net
useapi.netOP3mo ago
I can see that link 🙁
No description
Chaika
Chaika3mo ago
opps clipboard fail, it's just to the docs about duration https://developers.cloudflare.com/workers/platform/limits/#duration
There is no hard limit on the duration of a Worker. As long as the client that sent the request remains connected, the Worker can continue processing, making subrequests, and setting timeouts on behalf of that request. When the client disconnects, all tasks associated with that client request are canceled. Use event.waitUntil() to delay cancellation for another 30 seconds or until the promise passed to waitUntil() completes.
useapi.net
useapi.netOP3mo ago
Thank you for pointing out that sentence. I saw it but I did not interpret it correctly as I thought the tail calls threated as a separate call from the parent (which was a wrong assumption!). It will certainly help to clarify the help for the tail case.

Did you find this page helpful?