DeepSeek Harness web panel hangs on keepAliveTimeout
A plugin panel that spins forever after a few idle seconds is not a broken frontend state machine; the request was written onto a keep-alive connection the server was closing. When dsh-host-webserver creates the HTTP server it sets neither keepAliveTimeout nor headersTimeout, so Node's 5-second idle default applies. If the browser reuses the connection right at that boundary, it hits a close/send race and the request neither returns nor reports an error (Discussion #10106).
DeepSeek Harness web hang: reproduce with an 8-second idle connection
A raw socket separates "the server is healthy" from "the browser hangs". The reproduction script in the thread makes two requests against the dsh web port:
import http.client, time
c = http.client.HTTPConnection('127.0.0.1', 3080, timeout=10)
c.request('GET', '/'); print(c.getresponse().status)
time.sleep(8)
c.request('GET', '/'); print(c.getresponse().status)
- Send the first request and confirm a new connection returns 200 immediately. Expected: a normal status and the connection enters the keep-alive pool.
- Sleep 8 seconds and reuse the same connection. Expected: a
ConnectionResetError, or a wait until your own 10-second timeout. - Inspect the connection state with
lsof -nP -iTCP:3080. Expected:CLOSE_WAIT, meaning the server has closed and the client has not noticed. - Request the same path on a brand-new connection. Expected: immediate success, proving the server itself is not stuck.
Step 4 is the decisive check: new connections always work, and only reused idle connections hang. That places the fault in the connection layer, not in application code.
DeepSeek Harness correction: the browser is not unaware
Permanent loading is not caused by the client failing to know the connection is closing; it is caused by the close and the send landing at the same instant. Replies in the thread clarify two points:
- Node sends
Keep-Alive: timeout=5on every keep-alive response, and Chrome, Firefox, and Safari all use it to evict idle sockets early, so the browser is not blind. - What produces "the promise never settles" is the close/send race: the client writes just as the server timer fires, the bytes are accepted by a connection being dismantled, and no response is generated, so the client waits for its own timeout.
- The fix is still to set the timeouts, but they must satisfy
headersTimeout > keepAliveTimeout. Node does not enforce the relation; the semantics do. - Compression is not a host default:
DEFAULT_COMPRESSIONisnone, and gzip is enabled at the bundle layer, so changing the timeouts does not interact with compression.
That also explains why a refresh helps: it opens a new connection and bypasses the socket that is dying.
These clarifications do not change the fix but they do change verification. Do not decide the fix worked by whether the client shows a timeout; check whether a reused idle connection still gets a complete response. A client-side error only means the client reached its own deadline and says nothing about connection health.
DSH plugin path 1: set both timeouts on the server
This is the only root fix, and both values must be set together with the header timeout larger. Add them after createServer and before listen:
server.keepAliveTimeout = 60_000;
server.headersTimeout = 65_000; // must exceed keepAliveTimeout
- Confirm that a repository-wide search finds neither setting in the current host version. Expected: zero hits for
keepAliveTimeoutandheadersTimeout, which shows Node defaults are in use. - Add the two assignments where the webserver creates its server instance. Expected: the service starts normally with no parameter errors.
- Restart
dsh weband repeat the 8-second idle reuse test. Expected: the second request returns 200 instead of resetting or hanging. - Leave the page untouched for 30 seconds, then open a plugin panel. Expected: the panel loads without a refresh.
If your host version already exposes these values as configuration, change the configuration instead of the source; the test is whether the config schema defines the fields.
DeepSeek Harness path 2: client timeout plus one retry
This suits plugin authors as a temporary stopgap and should not be treated as a fix. The reporter added a 10-second AbortSignal.timeout and one automatic retry to their plugin; it does self-heal, but each occurrence wastes 10 to 20 seconds. If you write panel plugins, compare how others handle timeouts on DSH Plugin Hub:
- Add a 10-second
AbortSignal.timeoutto the panel'sfetchcalls. Expected: a hang no longer lasts indefinitely and throws a timeout error after 10 seconds. - On timeout, retry once on a new connection, and only for idempotent requests. Expected: the panel eventually loads, with a visibly slower first paint.
- Log the URL and elapsed time before retrying. Expected: you can measure how long recovery takes after each idle period and decide whether the stopgap is worth keeping.
Do not auto-retry non-idempotent plugin RPCs, or you may duplicate writes. Once the server is fixed, remove this layer rather than leaving both in place.
Keep retries to one or two. Every retry opens a new connection, so more attempts turn a 10-second hang into a 30-second wait and hide the fact that the server timeouts are unset.
DeepSeek Harness path 3: align the reverse proxy idle timeout
When dsh web sits behind nginx, Caddy, or a corporate gateway, the proxy recycles connections before the host does. Set the upstream idle time longer than the proxy's own:
- Read the proxy's idle settings:
keepalive_timeoutandproxy_read_timeoutfor nginx, thetimeoutsblock for Caddy. Expected: concrete seconds to compare with the new host values. - Set the upstream connection idle timeout above the host's
keepAliveTimeoutand make sure HTTP/1.1 upstream connections are used. Expected:proxy_http_version 1.1takes effect and theConnectionheader is no longer forced toclose. - Reload the proxy configuration and repeat the idle reuse test. Expected: requests through the proxy no longer hang.
- If the proxy cannot be changed, lower the host's
keepAliveTimeoutbelow the proxy threshold instead. Expected: the host closes first, so the proxy does not forward a half-dead connection upstream.
The common mistake here is changing the host and not the proxy, which moves the symptom from "hangs after 5 seconds" to "hangs after 60 seconds". It looks fixed, but the boundary only moved.
DeepSeek Harness: how to confirm the fix
There is one acceptance criterion: after idling past the old threshold, a reused connection still gets a complete response. Check in order:
- Run the raw socket script with 6, 30, and 55 seconds of idle time. Expected: all three return 200; success at 55 seconds shows the new idle threshold covers the full 60 seconds.
- Open the browser developer tools Network panel and watch the first request after an idle period. Expected: status 200, with no
(failed)and no long pending state. - Check the server for accumulated
CLOSE_WAITsockets. Expected: the count stays stable and does not grow with page idle time. - Run
curl --no-keepaliveagainst the panel endpoints repeatedly. Expected: every request uses a new connection and succeeds, which further isolates the problem to connection reuse.
Two easy misreads to keep in mind: the browser does know the connection may expire, because Keep-Alive: timeout=5 is in every response; and gzip is not slowing the response, because host compression defaults to none and bundle-level gzip is unrelated to the timeout.
According to the thread, the defect is still unfixed on the host side, and the reporter's in-plugin patch works, which confirms the diagnosis. Do not confuse it with gzip stream buffering: that stalls a response in the zlib buffer after it has been written, producing a stuttering render rather than permanent loading.
Source: Discussion #10106, deepseek-ai/deepseek-harness. For web auth and port issues, see DSH web auth and port.
FAQ
DeepSeek Harness uses Node's default 5-second keep-alive idle timeout. The server closes an idle connection after 5 seconds, and the browser may reuse that connection at the moment it is closing. The request bytes are accepted by a socket being torn down, no response is produced, and the frontend fetch never settles. A new connection always works, which makes it look like the panel is broken.
DeepSeek Harness requires headersTimeout to be larger than keepAliveTimeout. Otherwise a connection that just survived the idle window is cut off by the header timeout while the next request's headers are still on the way. Neither value is set in the host repository, so add both in one change; a common pair is 60000 and 65000.
No, a DeepSeek Harness panel retry only works around the problem. The retry moves the request to a new connection, so it appears to recover, but every occurrence wastes 10 to 20 seconds and retrying a non-idempotent plugin RPC can duplicate side effects. The root fix is to raise the server's idle timeout above the browser threshold.
DeepSeek Harness behind a reverse proxy needs both sides aligned, because the proxy has its own idle timeout and upstream connection pool. If nginx's keepalive_timeout or proxy_read_timeout is shorter than the upstream keepAliveTimeout, the proxy recycles the connection first. Set the upstream idle timeout longer than the proxy's, and confirm proxy_http_version 1.1 and the Connection header are correct.
Related Terms
- keepAliveTimeout
- keepAliveTimeout is how long an HTTP server waits for the next request on a keep-alive connection before closing it. Node's default is 5000 milliseconds.— https://github.com/deepseek-ai/deepseek-harness/discussions/10106
- headersTimeout
- headersTimeout is the deadline for a complete set of request headers to arrive. It must be greater than keepAliveTimeout, or a connection is terminated by the header timer just after its idle window.— https://github.com/deepseek-ai/deepseek-harness/discussions/10106
- close/send race
- A close/send race happens when a client writes a request onto a socket the server is about to close. The bytes are accepted but no response is ever produced, so the request hangs or the connection resets.— https://github.com/deepseek-ai/deepseek-harness/discussions/10106
- Keep-Alive: timeout=5
- The Keep-Alive response header tells the client how long an idle connection may be reused. Node includes it on every keep-alive response, and browsers use it to evict old sockets from their pools.— https://github.com/deepseek-ai/deepseek-harness/discussions/10106