Skip to content

Horizontal scaling: sessions and resume tokens are process-local #54

Description

@jason-shen

Problem

The server is single-node and nothing in the code says so out loud.

Session state lives in two process-local maps:

// internal/session/manager.go
sessions  map[string]*Session
resumable map[string]*Session

sessions holds the live PeerConnection, the running pipeline, the LLM conversation and the rolling summary. resumable indexes those same sessions by their current single-use resume token. Both die with the process, and neither is visible to any other instance.

Put two instances behind an ordinary round-robin load balancer and the recovery ladder documented in docs/protocol.md breaks in exactly the situations it exists for:

  • PATCH /whip/{sessionId} (ICE restart) lands on instance B, which has never heard of that session ID, and returns 404. The client burns its three backed-off restarts against the wrong box.
  • The resume redial carries a valid token that only instance A can validate. Instance B rejects it, and the client falls back to a fresh session: greeting replays, history gone. recovered-without-history becomes the normal outcome rather than the rare one.
  • server.max_sessions caps per process, so a 3-instance deployment silently has 3× the intended cap and 3× the provider spend.

None of this fails loudly. It fails as "reconnection doesn't work in production, works fine locally", which is the worst shape a bug can have.

Proposed change

Two options. They are not mutually exclusive and the first is a prerequisite for taking the second seriously.

A. Document sticky routing, and make the failure loud. The cheap, correct-today answer. Sessions are inherently stateful — the DTLS association and the SRTP keys are per-instance and no external store fixes that. What a second instance can do is route correctly:

  • A deployment guide covering session affinity in the common front ends (ALB target-group stickiness, nginx ip_hash/cookie, Cloud Run session affinity, and the caveat that WHIP's Location header is the natural affinity carrier since the client already echoes it back on PATCH and DELETE).
  • A knob to make the server advertise an absolute Location URL pointing at its own routable address, so a client's follow-up requests bypass the load balancer's choice entirely. This is the smallest change that actually fixes the problem.
  • max_sessions documented as per-instance, with the multiplication called out.
  • A distinct error on a PATCH for an unknown session, so "wrong instance" is distinguishable from "session expired" in a log.

B. External session/token store. For the case where affinity is not available. The resume token is the tractable half: it is a short-lived opaque string mapped to a session ID, and a shared Redis or Postgres index would let instance B answer "this token belongs to instance A" and issue a redirect. The conversation state (history, summary, transcript) would need to travel with it. The live peer never can.

Worth deciding explicitly whether B is in scope at all, or whether the answer is permanently "affinity, documented properly" plus a horizontally-scalable control plane in front. Either answer is fine; the current state — no answer written down anywhere — is not.

Acceptance criteria

  • docs/deployment.md (or a section in docs/configuration.md) states plainly that sessions are process-local and what breaks without affinity.
  • Affinity recipes for at least ALB, nginx, and Cloud Run.
  • Optional absolute Location advertisement, configurable, off by default.
  • max_sessions documented as per-instance.
  • PATCH /whip/{id} for an unknown session returns a distinguishable error and logs it as such.
  • A decision recorded on option B, even if the decision is "not planned".

Pointers

  • internal/session/manager.gosessions and resumable maps ~L29
  • internal/session/session.goresumeToken, single-use, reissued per resume ~L48
  • internal/signaling/ — WHIP handlers, Location header, PATCH route
  • docs/protocol.md — the recovery ladder this breaks
  • infrastructure/aws/ec2/ — the compose + Caddy deployment this would extend

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: configconfig.toml schema, validation, deploymenteffort: largeDesign discussion needed before codeenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions