When a non-blocking read hands back a stream of rows instead of a materialised list, what changes for the caller and the connection?
answer
- a recipe, not a value
- nothing runs until someone consumes
- the connection is held while you consume
- failures can arrive after row ten
basics
~20 sThe call returns before any statement runs: work starts when the caller consumes, rows arrive one by one, and errors can surface mid-sequence. The connection stays checked out for the whole consumption, so a slow consumer holds it.
solid answer
~50 sA materialised result is a value: by the time you hold it, the statement has finished and the connection is back in the pool. A streamed result is a **recipe** — typically nothing executes until something consumes it, rows are delivered as they are produced, and the consumer's pace controls how fast they come. Three consequences matter. The connection stays checked out for as long as consumption lasts, so per-row work inside the loop — a remote call, a write to another system — is now holding a pooled resource and can starve every other request. Failures no longer all arrive at the call site; a statement can fail after ten rows have already been handed over, so partial output may already be on the wire. And the sequence is usually single-use and cancellable: abandoning it early should release the connection, but only if the layer's cancellation actually reaches the statement.
go deeper
Hold on to the core difference: a materialised list is finished data, a streamed result is work that starts when you consume it and keeps the database connection busy while you do.
Be able to walk through deferred start, single use, the connection hold lasting as long as consumption, and errors arriving partway through a sequence rather than at the call.
Show the incident: per-row slow work inside an open read drains the connection pool and takes unrelated endpoints down. Talk about bounded buffering, real cancellation on client disconnect, and pool sizing for hold time.
Own the contract question. Decide per endpoint whether partial output is acceptable, and set a house rule about what may happen inside an open read, because that rule is what keeps the shared pool from becoming a hidden coupling.
## Two different things called "the result" When a read returns a **materialised** collection, the sequence is already over: the statement ran, every row was mapped, the connection went back to the pool, and the caller holds plain memory. When a read returns a **stream** — a sequence produced as rows arrive — the caller holds a description of work that has usually not started yet. This is the default shape in layers that may not block, because handing back a finished list would require waiting for the last row. Three properties follow, and each one changes how the calling code must be written. ## Deferred start, and usually single use Building the sequence typically issues no statement at all. The database is touched when something begins consuming it. Practical consequences: - A read that is built and then dropped does nothing — including a read that was supposed to have a side effect. - Consuming the same sequence twice generally re-runs the statement or fails outright; it is not a collection you can iterate again. If two consumers need the rows, materialise deliberately. - Timing measured around the call that *creates* the sequence measures nothing. Measure around consumption. ## The connection is held for the whole consumption This is the operational headline. A statement in progress owns a connection, and if the read runs inside a transaction it owns that too. With a materialised list, the hold time is the statement's own duration. With a stream, the hold lasts until the last row is consumed or the sequence is cancelled — and that is now under the *consumer's* control. | | Materialised list | Streamed result | |---|---|---| | When the statement runs | during the call | when consumption starts | | Connection hold time | the statement's duration | until consumption ends or is cancelled | | Memory | whole result in memory | roughly one row plus buffered rows | | Early exit | pointless, rows already fetched | cancels, releasing the connection | | Where errors appear | at the call | anywhere in the sequence | The classic incident is per-row work inside the consumer: calling another service, writing to a queue, or awaiting anything slow while a pooled connection sits checked out behind you. Under load, the pool empties and unrelated endpoints start timing out waiting for a connection. Two defences: **do not do slow work per row inside the consumption** — buffer to a bounded chunk, finish the read, then process — or accept the hold deliberately and size the pool for it. ## Errors and completion become events With a materialised read, either you get rows or you get a failure. With a stream, a failure can arrive after some rows have already been delivered — a connection dropped mid-result, a statement cancelled, a mapping error on row 500. If those rows were being written straight to a response, part of the answer is already on the wire and the failure cannot be turned into a clean error status. Decisions this forces: 1. Decide whether the endpoint may emit partial output. If not, buffer to a bounded size and only start writing once the read completed. 2. Give the sequence a termination signal your code actually handles — completed, failed, cancelled are three different endings. 3. Make cancellation real: a caller who disconnects should cancel the read, or the connection stays held for a result nobody wants. ## Choosing between the two shapes Streaming is right when the result is large or unbounded, when the consumer can act on rows independently, or when time-to-first-row matters more than total time. Materialising is right when the result is small and bounded, when the caller needs it more than once, when you want the connection released as early as possible, or when the consumer's work per row is slow enough to make holding a connection unacceptable. In many services the honest answer is a hybrid: stream out of the data-access layer, materialise into a bounded page immediately, and do everything else outside the read. One caution on ordering: a stream delivers rows in whatever order the statement produced them, and nothing about streaming makes that order stable. If the caller depends on ordering, the query must specify it explicitly with an ORDER BY.
- Why is doing a remote call per row inside the consumer worse than doing it after the read?Because the read is still open. The connection, and any transaction it carries, stays checked out for the whole loop, so each remote call's latency is added to the hold time. Under load the pool drains and unrelated requests block waiting for a connection. Buffer a bounded chunk, end the read, then call out.
- What does an early exit from consumption have to do to be safe?It has to actually cancel the underlying statement and release the connection, not merely stop reading. If cancellation does not reach the statement, the layer keeps draining rows or the connection stays held until the result is exhausted or times out, which is the same leak with a quieter symptom.
saying these in an interview costs you the question
- Assumes the statement already ran when the sequence was created
- Iterates the same streamed result twice expecting the same rows
- Does per-row remote calls while the read is still open
- Treats the streamed shape as automatically lower latency for a small result
- Assumes a failure can still be turned into a clean error after rows were emitted
- Expects a stable row order without asking for one in the query