python 89 lines · 3 tabs

Stream a Large CSV Export in Flask With a Generator Response

Shared by codesnips Jul 2026
3 tabs
from sqlalchemy import select

from .models import Order, db


def stream_orders(batch_size=1000):
    stmt = (
        select(
            Order.id,
            Order.customer_email,
            Order.total_cents,
            Order.status,
            Order.created_at,
        )
        .order_by(Order.id)
        .execution_options(stream_results=True)
    )

    result = db.session.execute(stmt).yield_per(batch_size)
    for row in result:
        yield (
            row.id,
            row.customer_email,
            row.total_cents / 100.0,
            row.status,
            row.created_at.isoformat(),
        )
3 files · python Explain with highlit

Exporting a large table as CSV is a classic place where a naive implementation quietly breaks in production: building the entire file in memory (or a list of rows) works fine for a thousand rows and then falls over at a few million, exhausting RAM and blocking the worker while the client waits for the whole payload. This snippet shows the streaming alternative, where Flask sends bytes to the client as they are produced instead of buffering the full response.

The key idea lives in orders_export.py. The route returns a Flask Response wrapping a Python generator, generate_rows, rather than a string. Flask (via Werkzeug) recognizes an iterable body and streams each yielded chunk down the socket, so memory stays flat regardless of table size. The generator uses csv.writer against a small in-memory StringIO buffer as a per-row line formatter: after writing a row it reads the buffer, yields the text, then truncates and rewinds the buffer so it never grows. Setting Content-Disposition: attachment and a filename turns the response into a browser download, and mimetype='text/csv' labels it correctly.

Crucially, the row source is itself lazy. stream_orders in order_repository.py uses SQLAlchemy's yield_per so the driver fetches rows in batches instead of materializing the full result set. This is what makes the whole chain O(1) in memory — a lazy DB cursor feeding a lazy generator feeding a streaming response. The repository yields plain tuples so the formatting layer stays decoupled from ORM objects.

The app setup in app.py wraps the generator in stream_with_context, which keeps the Flask application and request context alive for the duration of iteration. Without it, the database session bound to the request context could be torn down before the generator finishes producing rows, raising errors deep inside the stream.

A notable trade-off: once the first byte is sent, the HTTP status is already 200, so an error mid-stream cannot cleanly become a 500 — it can only truncate the download. Streaming also disables Content-Length, so clients see chunked transfer without a progress total. For very large, read-mostly exports those costs are usually worth the flat memory profile and the near-instant time-to-first-byte.


Related snips

Share this code

Here's the card — post it anywhere.

Stream a Large CSV Export in Flask With a Generator Response — share card
Link copied