Fix problem with large zip downloads#1984
Draft
stevecassidy wants to merge 2 commits into
Draft
Conversation
Signed-off-by: Steve Cassidy <steve@fieldnote.au>
stevecassidy
marked this pull request as draft
March 15, 2026 22:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fix problem with large zip downloads
Ticket
#1983
Description
We are seeing failures with large zip downloads (Full and attachments only). Requests are timing out/failing and the zip file does not complete. Also, once this has happened, subsequent downloads will also fail, even if they are not large. Restarting the server fixes the issue until the next failure.
Proposed Changes
Diagnosed with Claude's help, there is an issue with timeouts of requests to CouchDB and the way that we handle failures. Here is it's summary:
The core false assumption
The code assumes archiver processes entries in parallel — but it doesn't. Archiver serialises its internal queue: it reads one stream fully, writes it into the ZIP, and only then moves to the next. Despite the MAX_CONCURRENT_STREAMS = 15 name, what the code actually does is:
So you have 15 open CouchDB TCP connections, but only 1 is being read. The other 14 sit idle in archiver's queue, their connections open but unconsumed.
Initial trigger 1 — Idle connection timeout on queued streams
For a large file, archiver might spend 30-120 seconds reading a single attachment. While it does, the 14 queued streams are idle: CouchDB sent the first ~16KB (Node.js stream highWaterMark), then TCP flow control paused the rest. The TCP connection is open but silent.
Most CouchDB deployments sit behind nginx, a cloud load balancer, or CouchDB's own built-in idle timeout. Common values are 60 seconds (nginx default proxy_read_timeout) or 75 seconds. After crossing that threshold:
With the old code: throw in the archive.on('error', ...) handler leaves archive.finalize() permanently pending → hang
Initial trigger 2 — A per-record burst past the concurrency limit
There's a subtlety in the loop: the inner while checks activeStreams.size < MAX_CONCURRENT_STREAMS at the top of each iteration, but processRecordAttachments can add N promises in a single iteration without the limit being rechecked mid-record. If a record has 50 photos, activeStreams.size jumps from 10 to 60 in one step — and simultaneously 50 CouchDB HTTP requests fire. For projects with many high-attachment records this compounds the idle timeout problem significantly.
Why small subsequent requests also fail (before our fix)
With the old throw in the error handler: the uncaught exception leaves the Node.js HTTP response socket in a half-closed state that is never released. More critically, the open CouchDB nano HTTP connections hold sockets against the underlying http.Agent pool. If that pool's maxSockets is reached, all new CouchDB requests queue waiting for a free socket — permanently, since the stuck sockets are never freed without a restart.
What the fixes we made actually do
They fix the consequence: once a connection is time-killed, the error is now caught, all open source streams are destroyed (releasing their sockets), and archiver and the Express response are properly wound down. The archive exits cleanly with a 5xx response instead of hanging.
How to Test
Download large zip files - unfortunately the bug is only triggered intermittently so only deployment will test whether we have a fix.
Checklist