Timeouts and streaming responses
invoke defaults to 5 seconds, execution caps at 300, and results cap near 1 MiB — which limit a long task actually hits, and the order in which to diagnose context deadline exceeded. Includes the complete ctx.sse.send plus native Fetch parsing loop, and where streaming stops being the answer.
An ordinary invoke() waits for one complete JSON result and times out after 5 seconds by default. Model generation, slow third-party APIs, and anything that should appear while it is being produced all run into that line. This page covers two things: putting the timeout in the right place, and using native Fetch plus SSE for bounded incremental output.
Timeout configuration
invoke defaults to 5 seconds; the Func runner's default maximum execution time is 300 seconds. A bounded model generation or upstream request may set a longer caller timeout. This does not enable detached background work.
const result = await invoke('image.generate', input, {
timeoutMs: 120000,
})
Several hard limits apply at the same time and do not move when you raise the timeout:
| Limit | Value |
|---|---|
invoke default timeout | 5 seconds, overridden by the caller's timeoutMs |
| Maximum execution time | 300 seconds |
| Func result size | About 1 MiB; exceeding it fails the call — the first wall you hit when returning a whole table |
| Func code size | About 256 KiB |
Diagnosing context deadline exceeded
For context deadline exceeded or context timeout, do these three things in order:
- Inspect the actual
invoke(..., { timeoutMs })value in the page. This is almost always where the problem is — the caller is still on the 5-second default. - Retry the same input and model with a larger timeout.
creght func run --timeout_ms/run_func.timeout_msaffects that self-test only: passing there does not prove the production path passes, and failing there does not prove a platform hard limit. - If the larger value succeeds, raise the production caller's
timeoutMsand verify the complete path in the real page.
Do not treat shorter output, a lower
max_tokens, a different model or provider, a new table, or a fake background job as the timeout fix. Each of those changes the feature, when the actual problem is one number set too low.
Native Fetch + SSE streaming
For bounded incremental output, send events with ctx.sse.send and read them in the browser with native fetch and ReadableStream. No invokeStream wrapper is required.
// /backend/func/writer.ts
export async function main(input, ctx) {
ctx.sse.send('token', { text: 'Hello' })
ctx.sse.send('token', { text: ' world' })
return { ok: true }
}
The browser parses SSE frames itself. The key point is that reader.read() yields arbitrary byte chunks — a chunk is not guaranteed to be one event:
const response = await fetch('/func/writer?stream=1&timeout_ms=120000', {
method: 'POST',
headers: {
Accept: 'text/event-stream',
'Content-Type': 'application/json',
},
body: JSON.stringify(input),
})
if (!response.ok || !response.body) throw new Error('Func stream failed')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const frames = buffer.split(/\r?\n\r?\n/)
buffer = frames.pop() || ''
for (const frame of frames) {
const event = frame.match(/^event:\s*(.+)$/m)?.[1]
const data = frame.match(/^data:\s*(.+)$/m)?.[1]
if (event === 'token' && data) {
output += JSON.parse(data).text
}
}
}
- You must buffer across reads and split frames only on a blank line, keeping the trailing partial frame for the next chunk.
- Streaming is selected by
?stream=1&timeout_ms=...on the URL, not by aninvoke()option. - The platform sends the final
doneorerrorevent; handle both. - Cookies cannot change after the first event — headers are already committed. Anything that sets a session or cookie must happen before the first
send. - The call timeout still applies. Streaming is not an unbounded stream.
Streaming is not a background job
SSE lets the user see progress sooner, but the output still belongs to one request. Func cannot continue after the request ends: there is no setTimeout/setInterval, no job queue, and no "return now and finish later".
If the work genuinely exceeds 300 seconds, split it into steps the user can see: each step is one bounded call, intermediate state lives in a JSON table, and the page drives the next step — instead of leaving one call hanging.
Acceptance checklist
- Long-running calls set a
timeoutMsmatching the real duration in the production page, verified on the real path. - Results carry no bulk payload and stay well below the 1 MiB limit.
- SSE parsing buffers across reads, splits on blank lines, and handles
doneanderror. - Any cookie-setting logic completes before the first SSE event.
- No timers, polling, or fake background jobs are used to work around a timeout.
