DeepSeek Harness streams at 2fps inside gzip buffering
A phone mirror or emulator preview shows about 2fps with the picture jumping once every two seconds, yet the device renders at 27fps and the host writes each packet within 90ms — the encoder is not the suspect, gzip is. With compression enabled, the DeepSeek Harness (DSH) web server also compresses application/octet-stream streams, and the middleware sets no flush policy, so zlib holds data until its buffer fills or the response ends (source).
Decide whether DeepSeek Harness is slow or batching
Static scenes are worst, server writes are evenly spaced, and client reads are evenly spaced — all three together mean batching, not encoder performance.
- Timestamp every server write. Expected: intervals match the device frame rate, around 30ms, not two seconds.
- Log each client
reader.read()with its timestamp and byte count. Expected: reads about two seconds apart, each carrying a large chunk. - Print the response headers and look for
content-encoding: gzip. Expected: present, so the response was compressed. - Disable compression temporarily and retest the same stream. Expected: the frame rate returns to the device level.
- Check whether a reverse proxy compresses again. Expected: no extra buffering or encoding header.
| Look-alike | Distinguishing signal |
|---|---|
| Slow capture | server write intervals are long and uneven |
| Buffering middlebox | no gzip header, but reads still arrive in batches |
| keep-alive reconnect | the stream stops outright and logs a rebuild |
| gzip batching | even writes, gzip header, reads every two seconds |
The measurements in the discussion match exactly: a static scene made the encoder emit one large keyframe every two seconds, and the buffer released once every two seconds, so the stream looked like a 2fps encoder (#10024).
Why the DeepSeek Harness middleware accumulates the stream
The filter excludes only text/event-stream, application/octet-stream passes through, and the configuration sets level and threshold but no flush.
createGzipMiddlewareletscontent-rangeresponses through and skipstext/event-stream. Expected: everything else goes to the default filter.mime-dbmarksapplication/octet-streamas compressible, and unknown-length streams are explicitly allowed. Expected: the video stream enters the compressor.- The compressor has no flush option, so zlib emits only when its buffer is full or the stream ends. Expected: batching appears.
- The HTTP bridge calls
res.write(chunk)withoutres.flush(). Expected: data sits in zlib. - Every frame waits until the next keyframe fills the buffer. Expected: the client updates at keyframe cadence.
One easy-to-miss premise: compression: 'gzip' is not the web server default. The default is none, and a bundle turns it on, which is why the problem appears on some installs and fuels the "it works on another machine" misdiagnosis.
A static scene is the worst case because no small packets feed the compressor; only a large enough keyframe triggers output. More motion means denser packets and less visible batching, the opposite of the "bad network" intuition.
Path 1: add Z_SYNC_FLUSH to DeepSeek Harness compression
One configuration line makes every write immediately visible and benefits every streaming route.
- Find where the web server constructs the compression middleware and confirm it passes only
levelandthreshold. Expected: noflushfield. - Add
flush: zlib.constants.Z_SYNC_FLUSHto the same object. Expected: the middleware constructs and startup is clean. - Restart the host and retest the original route. Expected: the client reads each frame as it arrives.
- Watch bandwidth and CPU. Expected: a few extra bytes per chunk and roughly unchanged CPU.
- Roll it out on one profile first and keep a configuration backup. Expected: a one-command rollback exists.
- Remove the field and restart to roll back. Expected: behaviour matches the previous build.
- Retest a large file download. Expected: any compression-ratio change is acceptable.
With very small chunks, per-chunk flushing lowers the compression ratio. Merge flushes once per event-loop turn to trade a little throughput for latency.
Path 2: exclude the stream type from DeepSeek Harness compression
If the compressor should not touch streaming responses, skip that content type in the filter or give DSH plugin routes a "do not compress" marker.
- Declare a recognizable type for streaming routes instead of sharing
application/octet-streamwith binary downloads. Expected: the server can distinguish them. - Return false for that type in
filter. Expected: nocontent-encoding: gzip. - Keep compression for JSON and static assets. Expected: assets stay gzipped and sizes do not regress.
- Regression-test a page load and a file download. Expected: both behave normally.
- Document the new type. Expected: the next plugin route can find the rule.
- Verify one streaming route from a plugin. Expected: data arrives at write cadence.
This contract is cleaner but needs a new plugin-facing entry point. Once the type name ships, do not rename it, or older plugins silently return to being compressed.
Path 3: flush from the DeepSeek Harness writer
When the host cannot change, plugins can flush themselves, because the compression middleware attaches flush to the response object.
- Grab the response object in the write loop and check whether
flushexists. Expected: defined when compression is on. - Call
res.flush?.()after eachres.write(chunk), then handle backpressure. Expected: client reads correspond one-to-one with writes. - Write a composition test where the second chunk only arrives after the client decodes the first. Expected: it times out without the flush.
- Enable it only for streaming routes. Expected: ordinary large-file compression is unaffected.
- Log the flush count. Expected: it matches the number of writes.
No host change is needed, but every plugin must remember the call and the host default stays unfixed. The more plugins you run, the easier one is to miss.
Verify and set bounds for DeepSeek Harness streams
After the fix, measure time to first chunk and inter-chunk spacing, not average FPS, which keyframe cadence can mask.
- Record first-chunk time with compression on and off. Expected: with flush, both are the same order of magnitude.
- Confirm chunked transfer rather than a fixed
Content-Length. Expected: no length header on the stream. - Test a static scene. Expected: the worst input, and the one that exposes batching.
- Watch for five minutes. Expected: chunk spacing is steady, with no periodic stall.
| Path | Change site | Coverage | Main cost |
|---|---|---|---|
| Path 1 | one host middleware setting | every streaming route | slightly lower ratio |
| Path 2 | host filter plus a route marker | selected streaming routes | needs a plugin entry point |
| Path 3 | plugin write loop | one plugin | every plugin must call it |
Three limits apply: already-compressed payloads gain little, so do not expect a ratio win; a reverse proxy may compress again, so check it after changing the host; and no official fixed version appears in the discussion. For connection-layer stalls, see web performance and connection issues.
FAQ
DeepSeek Harness holds the response inside gzip, so the encoder is usually not the cause. The middleware sets no flush, so zlib waits until its buffer fills, and a static scene sends one large keyframe about every two seconds.
DeepSeek Harness enables gzip only when a bundle turns it on, so the default stays none. With compression on, unknown-length streams such as application/octet-stream are compressed too; with it off, data passes through. Check the response header first.
DeepSeek Harness adds a few flush bytes per chunk, negligible at 16 to 64 KB. The main cost is a slightly lower compression ratio, and already-compressed payloads such as H.264 barely notice; merge flushes per event-loop turn if chunks are tiny.
DeepSeek Harness stream buffering can be fixed on the write side, because the middleware attaches a flush method to the response. Calling res.flush after each write pushes the chunk out without a host change, but every plugin must remember to call it.
Related Terms
- Z_SYNC_FLUSH
- Z_SYNC_FLUSH is a zlib flush mode that emits all compressed data so far while keeping the compression dictionary for later input. It suits streaming responses without ending the whole stream.— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
- compression middleware
- The compression middleware is the wrapper the DeepSeek Harness web server uses for gzip, configurable by level, threshold, and filter. It selects by content type and length but does not flush zlib on the caller's behalf.— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
- chunked transfer
- Chunked encoding is HTTP/1.1 transfer without a Content-Length, sending data as it is produced. Streaming endpoints typically use it, which also lets the compressor decide when to emit.— https://github.com/deepseek-ai/deepseek-harness/discussions/10024
Sources
- #10024 — Web server gzip leaves streaming /api responses stranded in the zlib buffer· deepseek-ai (GitHub Discussions)
- DeepSeek Harness official repository· deepseek-ai (GitHub)