python 83 lines · 3 tabs

Stream a Large CSV Export in Chunks from a Flask Endpoint

Shared by codesnips Jul 2026
3 tabs
import csv


class _LineBuffer:
    def __init__(self):
        self._data = ""

    def write(self, value):
        self._data += value

    def take(self):
        out, self._data = self._data, ""
        return out


def iter_csv(rows, fieldnames):
    buffer = _LineBuffer()
    writer = csv.DictWriter(buffer, fieldnames=fieldnames, extrasaction="ignore")

    writer.writeheader()
    yield buffer.take().encode("utf-8")

    for row in rows:
        writer.writerow(row)
        chunk = buffer.take()
        if chunk:
            yield chunk.encode("utf-8")
3 files · python Explain with highlit

Exporting a large table as CSV over HTTP is a classic memory trap: building the whole file in a string or a list before returning it forces the entire dataset into RAM and blocks the worker until it finishes. This snippet shows the streaming alternative, where rows are produced lazily and flushed to the client in small chunks so memory stays flat regardless of table size.

In csv_stream.py, the core is iter_csv, a generator that yields already-encoded CSV byte chunks. Because Python's csv module writes to a file-like object, _LineBuffer acts as a tiny sink whose write just accumulates the current line; after each writerow the buffer is drained with take, so at most one row lives in memory at a time. The header row is emitted first, then rows arrive from an iterable of dicts. Yielding bytes (via .encode) matches what WSGI servers expect from a streaming response body.

The data source lives in db.py. fetch_rows opens a named cursor with connection.cursor(name=...), which in psycopg2 creates a server-side cursor. This is the key to true streaming from Postgres: without a name the driver buffers the full result set client-side, defeating the purpose. Setting itersize controls how many rows are fetched per network round trip, balancing round-trip overhead against memory. Iterating the cursor yields rows one batch at a time while the transaction stays open.

app.py wires it together. The route builds the row generator and hands iter_csv to Flask's Response with mimetype='text/csv'. Crucially the generator is wrapped in stream_with_context so the request and DB connection context remain alive while chunks are pulled lazily by the WSGI server. The Content-Disposition header prompts a file download, and a dated filename keeps exports distinguishable.

The main trade-off is that a streamed response can't easily change its status code or headers once bytes start flowing, so errors mid-stream are awkward — validation should happen before the first yield. It also holds a DB transaction open for the duration, so very slow clients can pin a connection. For bounded, download-style exports of large datasets this pattern keeps worker memory constant and lets users start receiving data immediately instead of waiting for the full file.


Related snips

Share this code

Here's the card — post it anywhere.

Stream a Large CSV Export in Chunks from a Flask Endpoint — share card
Link copied