Back to Feed
article 3m read

Streaming export read every row into memory before it sent one

The dashboard's export route gathered the whole result into memory before replying. The rewrite streams but never waits for a slow reader, and the plan check guarding it had its key respelled the same day.

Article DepthPractice

Flow

What I check on an export endpoint now

  1. 1Find the gathering

    Anything that collects the whole result before replying means it is not streaming.

  2. 2Wait for the reader

    Hand over the next chunk only when the reader asks, or chunks pile up on the server.

  3. 3Expect cut-off files

    Once the reply starts, a failure halfway can leave a short file that looks complete.

  4. 4Check the plan on the server

    Refuse in the route and only hide the button in the page.

Streaming was what the dashboard's export was meant to do. Instead the route read every row into memory, glued it into one big string, and only then sent the first byte.

Copies in memory

The first version had a JSON or CSV menu and a route behind it. The route did one thing well. It only exported your own data, and it refused if it couldn't tell whose data that was. Then it went wrong. The database handed back a stream, and the route collected all of it before replying. The JSON option made extra copies on top. So a big account could eat a lot of the server's memory, and the download didn't start until the last row was read.

That gathering step is the first thing I'd look for now.

The second version streams

A follow-up commit dropped JSON and kept the database's own CSV. It wrapped the database stream so bytes go out as they arrive. It also added a plan check. The route says forbidden if your plan doesn't include export, and the page hides the button.

It never waits for a slow reader

The wrapper pushes each chunk out as soon as it shows up. It never pauses when the reader falls behind. On a fast link that's fine. On a slow one, chunks pile up in a queue on the server, up to about one full copy of the export. I haven't measured this. I'm reading the code.

What you want is a stream that waits to be asked.

The stream asks the database for one chunk each time it's pulled, and closes when there are none left.

Two more catches. If the database fails halfway, the reply has already started as a success, so the user may keep a short file with no warning. And the button saves through a blob, so the whole file still ends up in the browser's memory. When the request fails, the user sees nothing but a spinner that stops.

An underscore, then a hyphen

The plan check looks up a key by name. The next commit changed one character in that name. Which spelling was right first, I couldn't say. But if the names don't match, everyone is denied. The button just isn't there, and as far as the route goes, nothing logs it.

The check also lets everyone through in development mode. So a wrong name works fine on your own laptop, because the real check never runs there.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward