Skip to course content
Free FastAPI backend course

FastAPI for AI Backend Development

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.