Choosing Ballerina’s HTTP Streaming API vs. Full‑Buffering for Large Payloads
When serving files >1 GB or handling high‑throughput uploads, Ballerina offers two patterns: stream‑based HTTP or in‑memory buffering. This guide compares constraints, trade‑offs, and shows a concrete implementation and validation workflow.
06 Jul 2026, 05:46 UTC

Decision Context
In a Ballerina 2201+ service that serves or receives large files, you can either stream the data using the stream type or buffer the entire payload in memory. The choice depends on payload size, client capabilities, and the desired balance between memory footprint and code simplicity.
Typical scenarios:
- Serving a 5 GB video file to a web browser.
- Accepting an upload from a sensor that pushes 1 GB of telemetry.
- Processing a 50 MB JSON payload that must be validated before storage.
Option Comparison
| Feature | Streaming (chunked) | Full‑Buffering |
|---|---|---|
| Peak memory usage | O(Chunk size) – typically <10 MB | O(Payload size) – can exceed JVM heap |
| Code complexity | Requires handling io:ReadableByteChannel and back‑pressure | Simple read‑into‑byte[] and return |
| Latency | Lower start‑up latency – first chunk sent immediately | Higher latency – full payload must be read before response |
| Back‑pressure support | Native – producer pauses if consumer is slow | No native back‑pressure – risk of blocking thread |
| HTTP/2 compatibility | Experimental; may lose order guarantees | Fully supported, no special handling |
| Client support | Requires chunked‑transfer support (most browsers, cURL) | Works with any client |
| Failure handling | Can stream partial data; client may abort mid‑stream | Single failure aborts entire transaction |
Trade‑Off Analysis
Choosing streaming is advantageous when:
- Payload >10 MB and you want to keep heap usage low.
- Clients can handle HTTP/1.1 chunked transfer.
- You need to start sending data before the entire file is ready (e.g., live video).
Full‑buffering is preferable when:
- Payload <10 MB – memory cost is negligible.
- You need atomicity (e.g., validate entire JSON before processing).
- Your client does not support chunked transfer.
Concrete Implementation
Streaming Service (Ballerina 2201+)
import ballerina/io;
import ballerina/http;
import ballerina/io;
import ballerina/file;
service fileService on new http:Listener(8080) {
@http:ResourceConfig { path: "/file/{fileName}" }
resource getFile(http:Caller caller, http:Request req, string fileName) {
// Resolve the file path – replace with your logic
string filePath = "/var/data/" + fileName;
var channel = file:openReadableChannel(filePath);
if (channel is io:ReadableByteChannel) {
// Set Content‑Length if known; otherwise chunked is used automatically
http:Response resp = new;
resp.setHeader("Content-Type", "application/octet-stream");
// Attach the stream
resp.setEntity(channel);
caller->respond(resp);
} else {
// Handle errors (file not found, permission denied, etc.)
http:Response errorResp = new;
errorResp.setStatusCode(404);
caller->respond(errorResp);
}
}
}
Key points:
- The
file:openReadableChannelcall returns anio:ReadableByteChannelthat Ballerina streams automatically using HTTP/1.1 chunked transfer. - No explicit loop over chunks is needed – the runtime handles back‑pressure.
- Set
Content‑Lengthonly if you know the size ahead of time; otherwise, the server will use chunked encoding.
Full‑Buffering Service
import ballerina/io;
import ballerina/http;
import ballerina/file;
service fileService on new http:Listener(8080) {
@http:ResourceConfig { path: "/file/buffer/{fileName}" }
resource getFileBuffered(http:Caller caller, http:Request req, string fileName) {
string filePath = "/var/data/" + fileName;
var data = file:readAllBytes(filePath);
if (data is byte[]) {
http:Response resp = new;
resp.setHeader("Content-Type", "application/octet-stream");
resp.setHeader("Content-Length", data.length.toString());
resp.setEntity(data);
caller->respond(resp);
} else {
http:Response errorResp = new;
errorResp.setStatusCode(404);
caller->respond(errorResp);
}
}
}
Here the entire file is loaded into a byte array before sending. This is simple but can exhaust the JVM heap for large files.
Validation & Monitoring
- Deploy both services. Run the Ballerina compiler with
bal runfor each service. - Measure heap usage. On Linux, find the process ID:
Then monitor heap before and after a request:pid=$(pgrep -f ballerina)jcmd $pid GC.heap_info | grep HeapSize - Trigger a request. Use cURL to download a large file:
For the buffered version:curl -O http://localhost:8080/file/largeFilecurl -O http://localhost:8080/file/buffer/largeFile - Compare peak heap. The streaming service should keep heap near baseline; the buffered service will spike by the file size.
- Load test. Use
wrkorabto issue concurrent requests and verify that streaming does not deadlock under back‑pressure:
Observe that response times stay stable and no timeouts occur.wrk -t2 -c50 -d30s http://localhost:8080/file/largeFile
Potential pitfalls:
- Clients that do not support chunked transfer may hang or receive incomplete data. Test with a simple HTTP client that prints raw headers.
- HTTP/2 support for streaming is experimental; if you need HTTP/2, prefer buffering or use a custom implementation that forwards the channel.
Conclusion
For payloads larger than 10 MB and when the client can handle HTTP/1.1 chunked transfer, Ballerina’s native streaming API is the most memory‑efficient and scalable choice. It automatically manages back‑pressure and keeps the JVM heap usage low. For smaller payloads, or when client compatibility or atomicity is critical, the simpler buffered approach remains appropriate. Validate your choice with the outlined monitoring steps to ensure that memory usage and latency meet your SLA requirements.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.