1.Formats, and streaming instead of building in memory
An `HttpResponse` builds the whole body in memory before anything is sent, so a 400 MB export needs 400 MB of process memory and the browser sees nothing until it is finished. `StreamingHttpResponse` takes an iterator instead and sends rows as they are produced. Django's documented CSV recipe pairs it with a tiny `Echo` class whose `write()` returns the value rather than storing it, which turns `csv.writer` into a generator. The format choice is separate: CSV for tabular data, JSON when structure matters, and Excel only when a person will actually open it in Excel.
Key features
- `HttpResponse` builds the entire body in memory before the first byte is sent
- `StreamingHttpResponse` "has no `content` attribute" — it has `streaming_content`
- The documented CSV recipe uses an `Echo` class that "implement[s] just the write method"
- Streaming avoids "a load balancer dropping a connection that might have otherwise timed out"
- An exception inside the generator cannot produce a 500 — headers are already sent
- `FileResponse(open_file, as_attachment=True, filename=…)` sets `Content-Disposition` for you
The three formats, and how each behaves under streaming
Which response class to return
- HttpResponse
- Every row is materialised before the first byte leaves
- Peak memory scales with the export — and multiplies per concurrent user
- The proxy sees a silent connection and may time it out
- The user stares at a blank tab for the whole build
- An error mid-build is at least an honest 500
- StreamingHttpResponse
- Each row is written and discarded
- Memory stays roughly flat at any row count
- Bytes flow immediately, so the connection stays visibly alive
- The download starts at once
- But: headers are already sent, so an error truncates instead of 500-ing
Command
StreamingHttpResponse((writer.writerow(r) for r in rows), content_type="text/csv")`HttpResponse` accumulates the whole body in memory; `StreamingHttpResponse` takes an iterator and stays flat, and it also keeps the connection visibly alive so a proxy does not time it out. Django's `Echo` class — a `write()` that returns instead of buffering — is what turns `csv.writer` into a generator. Stream the queryset too, with `.iterator()`, or the laziness stops at the database. And remember the trade: headers go out first, so an exception inside the generator truncates the file rather than raising a 500. Do everything fallible before the response object exists.