session.counter
0
useState, minus the client
React is UI = f(state), with a runtime that re-runs f when state changes. This page is the same equation: the state is a Rust struct in a map, the setter is a POST, and the re-render is the response to the redirect that follows it.
session.counter
0
session.todos
0 left
Nothing here yet. Add an item and note that it survives navigating away and coming back — but not a server restart, which is the whole argument for a shared store.
POST → 303 → GETEach button is a real form. The handler mutates the session, redirects, and the browser re-requests the page — which is why a refresh never resubmits anything.
Read, write, re-render. The difference is what triggers the re-render.
React
const [count, setCount] = useState(0)
<button onClick={() => setCount(count + 1)}>
{count}
</button>
// setCount schedules a re-render
// of this component.axum + maud
async fn page(session: Session) -> Markup {
view(session.get().counter)
}
async fn bump(session: Session) -> Redirect {
session.update(|s| s.counter += 1);
Redirect::to("/state") // re-render
}the runtime is the browserReact's runtime re-invokes your component; here the browser re-requests the document. Same equation, different scheduler — and a round trip instead of a microtask.
This page keeps it in server memory behind a session id. The alternative is to encrypt the state itself into the cookie and keep the server stateless. Both are defensible; they fail differently.
| Session id + server store | Encrypted cookie (or JWT) | |
|---|---|---|
| Size limit | None — it never leaves the server. | ~2.9 kB after base64 and AEAD overhead. |
| Cost per request | One id (32 bytes) plus a map lookup. | The whole payload, on every request to the origin. |
| Revocation | Delete the entry; the next request is anonymous. | Impossible. Valid until it expires. |
| Replay / rollback | Server is authoritative; old copies mean nothing. | A saved cookie can be restored to rewind state. |
| Two tabs at once | One copy, lockable, last write is coherent. | Each tab holds its own copy; last write clobbers. |
| Server restart | Lost, unless the store is Redis or a database. | Survives — the state was never on the server. |
| Best for | Auth, permissions, anything counted or sensitive. | Theme, locale, dismissed banners, form drafts. |
4096 bytesRFC 6265 asks browsers to support only 4 kB per cookie, counting name and attributes — and the cookie rides along on every request to the origin, static files included.
4096 RFC 6265 per-cookie floor
-66 name + Path/HttpOnly/SameSite/Max-Age
────
4030 bytes for the value
×3/4 base64 (3 bytes -> 4 chars)
────
3022 bytes of ciphertext
-28 AEAD nonce (12) + tag (16)
────
~2.9 kB of serialized state, total.
Budget under 1 kB in practice.An encrypted cookie cannot be revoked — it stays valid until it expires, so you cannot log anyone out — and it can be replayed: a user can restore an old copy and rewind their own state. Fine for a theme; not fine for a credit balance.
A JWT is the same trade with a standard envelope. Signed, not encrypted, so the payload is readable. Its real advantage is verification with a public key across services — which a monolith issuing its own cookies does not need.