Sessions & state¶
Understanding where state lives is the key to deploying Golit. It's one idea, and everything about scaling follows from it.
What a session is¶
Each client gets a server-side session, keyed by the golit_session cookie. A session holds two things:
- a kernel graph — this client's node states and memo hashes, and
- a value registry — this client's current node values (the Polars frames, scalars, and rendered fragments).
The App blueprint — the topology, the functions — is shared across all sessions. Only the state is per-session. So two users moving sliders independently each have their own filtered frames and their own clean/dirty bookkeeping, over one set of node definitions.
State is worker-local — by design¶
A session lives in the memory of the worker that created it. It is not written to a shared store on each interaction. That locality is deliberate: it's exactly what makes recompute cost track the change. Serializing DataFrames to Redis or a database on every slider move would throw that away and reintroduce the cost Golit exists to avoid.
The default rule
A client's requests are cheapest on the worker that already holds its session — the initial GET /, each POST /node/…, and the long-lived GET /events stream. That's session affinity ("sticky sessions"), the recommended default the deployment story is built around. With a Redis session store it stops being a hard rule: the input state is durable, so a request that lands elsewhere reconstructs the session rather than losing it.
Lifecycle¶
| Event | What happens |
|---|---|
First GET / with no/unknown cookie |
A session is created, the cookie is set, and the graph is computed once (initial render). |
POST /node/{id} |
The input is coerced and stored; the dirty subgraph runs; changed fragments return. |
GET /events |
The session's SSE stream opens; server-pushed fragments flow to this client. |
| Worker restart | That worker's warm caches are gone. Clients re-render from defaults next GET / — or, with a Redis session store, reconstruct from their stored inputs. |
A session is always reconstructible — that's the point. Without a session store, a returning client whose session is missing just renders from defaults again. With one (RedisSessionStore, via GOLIT_REDIS_URL), the worker rebuilds the session from the client's stored inputs (replay + recompute), so state survives a worker restart, a rebalance, or a request that lands on a different replica. Only the inputs are persisted — never the frames.
Keep sources cheap and idempotent
Because a session can be rebuilt at any time (worker restart, new worker), your @app.source functions should be inexpensive and side-effect-free to call again. Treat the initial render as something that can happen more than once.
Memory sizing¶
Memory is dominated by live session values — roughly the size of your Polars frames times the number of active sessions. When you size and scale workers, that's the figure to watch. Two practical levers: keep per-session frames lean (filter/aggregate early), and scale replicas on memory headroom.
Where this leads¶
Two consequences define how Golit scales horizontally:
- Affinity. Keep each client on one worker (the load balancer hashes the session cookie) — the recommended default; the Redis session store turns it into a warm-cache optimization rather than a correctness requirement.
- Fan-out. A server-side invalidation must still reach whichever worker holds each affected client's SSE stream — that's Redis pub/sub.
Both, and the exact topology, are in Deployment & scaling.