Streaming
Node and Bun Functions stream server-sent events to the client as they are produced, with no flag to set.
A Node or Bun Function streams server-sent events by default. A text/event-stream response goes out as your code writes it, so live updates and progress events reach the browser as they happen instead of landing in one block at the end. Responses with other content types, such as text/html and text/plain, reach the client in one piece once your Function has finished writing them.
What it looks like
// api/events.js
import http from 'node:http';
const server = http.createServer((req, res) => {
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-store',
});
let tick = 0;
const timer = setInterval(() => {
tick += 1;
res.write(`data: ${JSON.stringify({ tick, at: new Date().toISOString() })}\n\n`);
if (tick === 10) {
clearInterval(timer);
res.end();
}
}, 1000);
req.on('close', () => clearInterval(timer));
});
server.listen(3000);version: 4
functions:
api/events.js:
runtime: node-22
max_duration: 60
routes:
- source: ^/events$
destination: /api/events.jsEach res.write of an event stream is flushed on its own rather than buffered until the handler returns, so the client sees the first event about a second after connecting. Gigadrive Network captures the server your code creates and assigns it a port, so the number you pass to listen is ignored.
Response headers
The response body passes through untouched. Caching and compression headers are handled at the edge:
- A streamed response you want cached needs an explicit
cache-control. A Function that sets none getscache-control: no-store. - Compression is negotiated at the edge, not in your Function. Your code never sees the client's
accept-encoding, and the CDN compresses the response and sets its owncontent-encodingandcontent-length. Write plain bytes.
Where a stream ends
A stream that overruns is truncated, not failed
The invocation deadline covers the response body as well as the headers. If a stream is still running when max
duration expires, the status line has already been sent and cannot be changed, so
the body stream is errored instead. The client sees a 200 that stops early. The same is true of the 32 MiB response
ceiling: the connection closes without its terminating chunk, which is how a client detects the truncation.
Set max_duration to something above the longest stream you expect, and close the stream from your own code when a client disconnects. In the example, req.on('close', ...) stops the interval when a browser navigates away; without it the Function keeps producing events nobody reads until the deadline fires.
A WebSocket is the better fit when the client needs to send as well as receive.
