Cloud Functions timeout behavior: when cleanup code is skipped after timeout
0 reputation · 29 Jun 2026, 10:09 UTC
0 reputation · 29 Jun 2026, 10:09 UTC
Firebase Cloud Functions impose a hard timeout that terminates the execution thread when exceeded. In 1st‑gen functions the default is 60 s, extendable to 540 s; 2nd‑gen HTTP functions can run up to 3600 s. When the timeout fires, the runtime stops the function instance and any code scheduled after that point is not guaranteed to run. Consequently, cleanup logic placed after a long‑running operation may be silently dropped.
Additionally, a client aborting an HTTPS request does not signal the function to stop; the server continues until it reaches its timeout or finishes. There is no built‑in cancellation token that propagates back to the handler.
Given these constraints, developers often checkpoint partial work to Firestore or offload remaining work to Cloud Tasks, but the exact boundary of guaranteed execution remains unclear.
What mechanisms can reliably detect that a timeout has occurred so that cleanup can be performed before the instance is reclaimed? How can a function be designed to react to a client disconnect without incurring unnecessary billed time?
30275 reputation · 29 Jun 2026, 15:35 UTC
There is no callback that fires when a Cloud Functions timeout occurs — the runtime terminates the instance abruptly, so no code after that point (including finally blocks) is guaranteed to run. The reliable approach is to detect the timeout before it happens by polling the remaining execution time, and to detect client disconnects by listening for the request close event so you can exit early.
Firebase Cloud Functions enforce a hard execution deadline (60 s default in 1st gen, configurable up to 540 s; 2nd-gen HTTP functions can run up to 3600 s). When the deadline fires, the instance is reclaimed without running your remaining code. This means:
try/finally and process-level hooks are only dependable for synchronous errors and explicit returns, not for timeout termination.Track the deadline yourself and bail out with a safety margin before the runtime kills you:
const DEADLINE_MS = 540_000; // match your configured timeout
const SAFETY_MS = 5_000;
exports.longJob = functions.runWith({ timeoutSeconds: 540 })
.https.onRequest(async (req, res) => {
const deadline = Date.now() + DEADLINE_MS - SAFETY_MS;
for (const item of workItems) {
if (Date.now() > deadline) {
// Persist progress and hand off the rest
await db.doc(`jobs/${jobId}`).set({ cursor: item.id }, { merge: true });
await enqueueContinuationTask(jobId); // Cloud Tasks
await cleanup(); // runs while time remains
res.status(202).json({ status: 'continued' });
return;
}
await processItem(item);
}
res.status(200).json({ status: 'done' });
});Key points:
In the Node.js runtime, the request object emits close when the client goes away. Use it to stop early and avoid billed idle time:
let clientGone = false;
req.on('close', () => { clientGone = true; });
for (const item of workItems) {
if (clientGone) {
await cancelPendingWork(); // abort unawaited promises, streams, etc.
return; // exit early; no response needed
}
await processItem(item);
}Caveats:
close can also fire after a normal response completes, so only treat it as an abort if you haven't sent the response yet.finally — you should see only the first log, confirming post-timeout code is dropped.curl --max-time 2) and confirm the close handler fires and billed execution time shrinks.One detail worth confirming for your case: which runtime and generation you're on — the remaining-time API and request event semantics differ between Node.js, Python, and Go, and between 1st and 2nd gen.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.