Unit 11.03: Streaming a response without holding it all
Streaming keeps memory flat and gives up the ability to fail cleanly.
One row at a time
A CSV export produced by a generator.
The code consumes it in chunks.
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/export")
def export() -> StreamingResponse:
def rows():
yield "id,amount\n"
for i in range(1, 6):
yield f"INV-{i},{i * 100}\n"
return StreamingResponse(rows(), media_type="text/csv")
client = TestClient(app)
with client.stream("GET", "/export") as r:
chunks = list(r.iter_text())
print(f"chunks received: {len(chunks)}")
print("".join(chunks))
print("""
The generator produces one row at a time, so memory stays flat whether the
export is five rows or five million.
The cost is that the status code is sent before the body -- so a failure
halfway cannot become a 500. The client receives a truncated file, which is
why a streamed export needs a terminator the client can check for.
""")
Memory stays flat whether the export is five rows or five million, because only one row exists at a time.
The cost is that the status code and headers are sent before the body. A failure halfway through cannot become a 500 - the client receives a truncated file with a 200 status, which is why a streamed export needs a terminator the client can check for.
The mistake this prevents
The mistake is streaming and assuming errors still surface normally. They cannot: the response has already started. Validate everything you can before the first chunk goes out.
Takeaway
Streaming keeps memory flat and commits the status code before the body. Validate up front, and give the client a way to detect truncation.
