{"record":{"id":"60bfa157e41a7cee","repo":"plandex-ai/plandex","slug":"connection-to-plan-stream-timed-out-due-to-missing","errorCode":null,"errorMessage":"connection to plan stream timed out due to missing heartbeats","messagePattern":"connection to plan stream timed out due to missing heartbeats","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/cli/api/stream.go","lineNumber":29,"sourceCode":"\t\"time\"\n\n\tshared \"plandex-shared\"\n)\n\n// 3 heartbeat misses = timeout\nconst HeartbeatTimeout = 16 * time.Second\n\nfunc connectPlanRespStream(body io.ReadCloser, onStream types.OnStreamPlan) {\n\treader := bufio.NewReader(body)\n\ttimer := time.NewTimer(HeartbeatTimeout)\n\tdefer timer.Stop()\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\t\tlog.Println(\"Connection to plan stream timed out due to missing heartbeats\")\n\t\t\t\tonStream(types.OnStreamPlanParams{Msg: nil, Err: fmt.Errorf(\"connection to plan stream timed out due to missing heartbeats\")})\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\ts, err := readUntilSeparator(reader, shared.STREAM_MESSAGE_SEPARATOR)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Error reading line:\", err)\n\t\t\t\tonStream(types.OnStreamPlanParams{Msg: nil, Err: err})\n\t\t\t\tbody.Close()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ttimer.Reset(HeartbeatTimeout)\n\n\t\t\t// ignore heartbeats\n\t\t\tif s == string(shared.StreamMessageHeartbeat) {\n\t\t\t\tcontinue","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/cli/api/stream.go#L11-L47","documentation":"This error is produced by connectPlanRespStream in app/cli/api/stream.go when the plan SSE-style response stream receives no messages (including heartbeat frames) for HeartbeatTimeout (16 seconds, roughly 3 missed heartbeats). The reader goroutine arms a timer and resets it on every message; if the timer fires, it reports this error through the onStream callback and closes the HTTP response body. It indicates the server stopped sending data or the connection silently stalled without an I/O error.","triggerScenarios":"During an active plan stream (connectPlanRespStream), the readUntilSeparator call blocks for >16s with no message arriving — the server never resets the timer, so it fires and the goroutine calls onStream with this error.","commonSituations":"Server under heavy load or hung LLM backend stops emitting heartbeats; a proxy/firewall/load balancer silently drops or buffers the long-lived streaming connection; network outage that doesn't surface as a read error; server crashed mid-stream without closing the TCP connection.","solutions":["Check network path (VPN, proxy, corporate firewall) for idle/streaming connection timeouts and raise or disable them for the API host.","Retry the plan request — transient server stalls usually resolve; enable any available retry/resume logic in the CLI.","Verify the Plandex server is healthy and not overloaded (restart or scale the server) since stalled heartbeats often mean the backend hung.","Upgrade both CLI and server versions to ensure heartbeat intervals and timeout (HeartbeatTimeout=16s) are compatible."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Before starting a long plan stream, verify reachability and avoid idle-killing intermediaries\nif err := checkServerReachable(serverURL); err != nil {\n    return fmt.Errorf(\"server unreachable before streaming: %w\", err)\n}\n// ensure proxies keep the connection alive: disable idle timeouts / enable TCP keepalive on the http.Client transport","typeGuard":"// Narrow the error surfaced via OnStreamPlanParams\nfunc isHeartbeatTimeout(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"timed out due to missing heartbeats\")\n}","tryCatchPattern":"// In the OnStreamPlan callback\nif params.Err != nil {\n    if isHeartbeatTimeout(params.Err) {\n        // close/reconnect: retry the stream or resume the plan rather than treating it as fatal\n        retryWithBackoff(func() error { return startPlanStream(onStream) })\n        return\n    }\n    handleFatal(params.Err)\n}","preventionTips":["Keep the connection path free of proxies/firewalls with aggressive idle timeouts","Use TCP keepalive on the HTTP transport used for streaming","Monitor server health; stalled heartbeats usually mean a hung backend","Retry transient stream failures with backoff instead of failing the whole plan"],"tags":["network","streaming","timeout","go"],"backgroundTag":"stream-heartbeat-timeout","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}