Streaming
EdgeZero supports streaming responses for large payloads, real-time data, and server-sent events.
Streaming Responses
Use Body::stream to yield response chunks progressively:
use edgezero_core::action;
use edgezero_core::body::Body;
use edgezero_core::http::Response;
use bytes::Bytes;
use futures::stream;
#[action]
async fn stream_data() -> Response {
let chunks = vec![
Bytes::from_static(b"Hello"),
Bytes::from_static(b" "),
Bytes::from_static(b"World"),
];
let body = Body::stream(stream::iter(chunks));
Response::builder()
.status(200)
.header("content-type", "text/plain")
.body(body)
.unwrap()
}How Streaming Works
The core router passes Body::Stream through untouched; whether the client sees chunks progressively depends on the adapter:
- Your handler returns
Body::stream(...)with aStreamof chunks - Cloudflare wraps the stream in a
ReadableStream(Response::from_stream), so chunks reach the client as they are produced - Spin and Axum collect the stream into a buffer, and Fastly's standard
run_app/serve_appdispatch writes each chunk into a host-side body that is only sent once complete (Spin rejects streamed bodies over 16 MiB), so on all three the client receives the whole body at once
On Cloudflare, streaming also keeps memory flat, because each chunk is forwarded as it is produced. On the buffering adapters a streamed body is collected in full before the response goes out: Axum into an unbounded buffer, Spin into a buffer capped at 16 MiB (larger bodies fail), and Fastly into a host-side body that is only sent once complete. There Body::stream is an API-composability convenience, not a memory saving, and a very large streamed response can exhaust memory or be rejected. For progressive delivery (SSE or long-lived chunked responses), use Cloudflare's standard dispatcher or Fastly's custom lifecycle::serve_custom callbacks. The Fastly callback must explicitly commit, pump and flush chunks, and finish the writer. Convert with request::into_core_request_with_registries (the bare into_core_request inserts no store registries) and drive app.router().oneshot(..) yourself, so the core response stays available for streaming.
Server-Sent Events
Stream events to clients with SSE:
use edgezero_core::action;
use edgezero_core::body::Body;
use edgezero_core::http::Response;
use bytes::Bytes;
#[action]
async fn events() -> Response {
let events = async_stream::stream! {
for i in 0..10 {
let payload = format!("data: Event {}\n\n", i);
yield Bytes::from(payload);
}
};
Response::builder()
.status(200)
.header("content-type", "text/event-stream")
.header("cache-control", "no-cache")
.body(Body::stream(events))
.unwrap()
}Body Modes
Routes can specify their body handling mode in the manifest. This is parsed today and reserved for future enforcement by adapters and router helpers:
[[triggers.http]]
path = "/upload"
methods = ["POST"]
handler = "my_app::handlers::upload"
body-mode = "buffered" # or "stream"| Mode | Behavior |
|---|---|
buffered | Body is fully read into memory before handler runs |
stream | Body is passed as a stream for progressive processing |
Transparent Decompression
On Fastly, Cloudflare, and Spin, EdgeZero automatically decompresses gzip and brotli responses from upstream services (the Axum proxy client has no decode path):
// Proxied response with Content-Encoding: gzip is automatically decoded
let response = proxy.forward(request).await?;
// response.body is now decompressedThis happens transparently in the adapter layer using shared decoders from edgezero-core.
Memory Considerations
On paths that forward chunks as they are produced (Cloudflare's standard dispatcher and Fastly lifecycle::serve_custom callbacks; see How Streaming Works), streaming is what makes these workloads fit:
- Large file downloads
- Video/audio content
- Real-time data feeds
- Responses larger than available memory
Platform Limits
Edge platforms have memory constraints. A Fastly Compute instance has ~128MB by default. On Cloudflare, stream large responses rather than buffering. On Fastly's standard run_app/serve_app paths, Spin, and Axum a streamed body is still collected in full before it is sent, so keep responses within the instance's memory regardless of how you build the body. Spin additionally caps streamed-body collection at 16 MiB; an already-buffered Body::Once bypasses that cap. Fastly's custom serving callbacks can instead stream progressively with explicit sending.
Chunked Transfer
When the response size is unknown, Body::stream lets the adapter send without a Content-Length. Cloudflare delivers this as chunked transfer; the buffering adapters compute the length once the body is collected. Fastly custom serving callbacks may also commit a native streaming response; see Custom dispatch and streaming. For example:
#[action]
async fn dynamic_content() -> Response {
let stream = generate_content_stream();
// No Content-Length header needed
Response::builder()
.status(200)
.header("content-type", "application/octet-stream")
.body(Body::stream(stream))
.unwrap()
}Next Steps
- Learn about Proxying for forwarding requests upstream
- Explore adapter-specific streaming in the Fastly, Cloudflare, and Spin guides