Skip to content

Add WebSocket heartbeat with pong-timeout dead-connection detection - #7

Merged
partouf merged 1 commit into
mainfrom
websocket-heartbeat-ping-pong
Aug 9, 2026
Merged

Add WebSocket heartbeat with pong-timeout dead-connection detection#7
partouf merged 1 commit into
mainfrom
websocket-heartbeat-ping-pong

Conversation

@partouf

@partouf partouf commented Aug 9, 2026

Copy link
Copy Markdown
Member

Problem

The WebSocket ping was fire-and-forget — it sent frames every 30s but never verified a response. A silently dead connection (half-open TCP, dropped network, a load balancer reaping an idle connection) went undetected until the OS TCP stack gave up, which can take minutes. During that window isConnected() and the healthcheck reported healthy while compilation subscriptions silently hung until their per-request timeout.

Change

  • Active heartbeat with pong-timeout. Each interval sends a ping and arms a pong-timeout. If no pong or other inbound activity arrives before the deadline, the socket is treated as dead and terminate()d, which triggers the existing reconnect path. Dead connections are now caught in ~pingInterval + pongTimeout (≈40s) instead of minutes.
  • Activity-based liveness. Any inbound message also counts as proof-of-life, so a busy connection is never killed for a single missed pong.
  • PingMode setting (--ping-mode / WEBSOCKET_PING_MODE, default application):
    • application — sends a "ping" text message; the events server replies with a "pong" text message. This round-trips through the Lambda backend = true end-to-end liveness.
    • control — protocol-level ws.ping() / 'pong'. Behind API Gateway these are answered at the AWS edge, so they only prove the front door is up, not that the Lambda is reachable.
  • Healthcheck now also reports lastActivityTime and secondsSinceLastActivity.

Why app-level is the default

API Gateway WebSocket APIs don't surface protocol ping/pong control frames to the Lambda backend. The matching server-side handler (events-lambda, message === 'ping''pong') has been deployed to prod/staging/beta and verified: text "pong" round-trips on all three stages (prod ~267ms, staging ~177ms, beta ~894ms cold) — comfortably within the 10s pongTimeout.

Tests

New unit coverage for: ping cadence, termination on silence, staying alive under text "pong" replies, staying alive under application messages, reconnect-after-timeout, heartbeat stop on close, and control mode (protocol ping + protocol pong). 100 tests pass, typecheck and lint clean.

🤖 Generated with Claude Code

The previous ping was fire-and-forget: it sent frames but never verified a
response, so a silently dead connection (half-open TCP, dropped network, LB
reaping an idle connection) went undetected until the OS TCP stack gave up
minutes later. During that window isConnected()/healthcheck reported healthy
and compilations silently timed out one by one.

The heartbeat now sends a ping every interval and arms a pong-timeout; if no
pong or other inbound activity arrives before the deadline, the socket is
treated as dead and terminate()d, which triggers the existing reconnect path.
Any inbound message also counts as liveness, so a busy connection is never
killed for a missed pong.

The events server implements keepalive at the application level (it replies to
a "ping" text message with a "pong" text message), because API Gateway
WebSocket APIs answer protocol-level ping/pong at the AWS edge -- which proves
only that the front door is up, not that the Lambda backend is reachable. A
configurable PingMode (--ping-mode / WEBSOCKET_PING_MODE, default "application")
selects text ping/pong (true end-to-end liveness) or protocol ping/pong.

Healthcheck now also reports lastActivityTime / secondsSinceLastActivity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@partouf
partouf merged commit 5a01103 into main Aug 9, 2026
2 checks passed
@partouf
partouf deleted the websocket-heartbeat-ping-pong branch August 9, 2026 18:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant