python 77 lines · 3 tabs

Stream a Large CSV Export in FastAPI With StreamingResponse and a Generator

Shared by codesnips Jul 2026
3 tabs
from sqlalchemy import select
from sqlalchemy.orm import Session

from .models import User

BATCH_SIZE = 1000


def stream_users(session: Session):
    stmt = (
        select(User.id, User.email, User.full_name, User.created_at)
        .order_by(User.id)
        .execution_options(yield_per=BATCH_SIZE)
    )
    # server-side batched fetch; rows arrive lazily, not all at once
    for row in session.execute(stmt):
        yield row
3 files · python Explain with highlit

This snippet shows how a FastAPI endpoint can export a very large table as CSV without buffering the whole file in memory. The core idea is that StreamingResponse accepts an iterator (or generator), and FastAPI drains it chunk by chunk onto the socket. As long as each chunk is produced lazily and the underlying data source is streamed too, memory stays flat regardless of whether the export is a thousand rows or ten million.

In csv_stream.py, the iter_csv generator is the heart of the technique. Instead of building a list of rows and calling writer.writerows, it wraps a small in-memory buffer with io.StringIO, writes a single logical row into it, yields the buffered text, then truncates the buffer with seek(0)/truncate(0). That truncation is what keeps the buffer bounded: the StringIO is reused as a formatting scratchpad rather than an accumulator. Using csv.writer matters here because it handles quoting, embedded commas, and newlines correctly — hand-rolling ','.join would corrupt data containing those characters.

The generator pulls its data from stream_users in repository.py, which uses SQLAlchemy's yield_per to fetch rows in server-side batches. This is critical: without it, the driver would materialize the entire result set before the first row is ever yielded, defeating the whole purpose. yield_per sets a fetch size so the database and generator stay in lockstep, giving natural backpressure — the query only advances as fast as the client consumes bytes.

In routes.py, the export_users endpoint returns a StreamingResponse built from iter_csv(session), with media_type set to text/csv and a Content-Disposition header so browsers download it as a file. Note there is no Content-Length; the response uses chunked transfer encoding because the total size is unknown up front.

One pitfall worth calling out: the database session must stay open for the entire duration of streaming, so the session is created inside the request and closed only after the generator is exhausted, not via a short-lived dependency. This pattern is the right reach whenever an export is large enough that loading it fully would risk memory pressure or slow time-to-first-byte.


Related snips

Share this code

Here's the card — post it anywhere.

Stream a Large CSV Export in FastAPI With StreamingResponse and a Generator — share card
Link copied