Canva: Stateless Session Cookies and Revocation at Hundreds of Millions of Users
Problem + constraints
Every backend request at Canva needs to know which logged-in user made it — their user ID, permissions, and roles. At Canva's scale (260M+ users), that question gets asked hundreds of thousands of times per second. The naive answer — look the session up in a networked datastore on every request — puts a shared, latency-sensitive dependency directly in the hot path of every single API call, and turns that datastore into both a bottleneck and a single point of failure for the entire product.
The opposite naive answer — a fully stateless, self-contained token that the gateway can verify on its own — solves the latency and availability problem but creates a much harder one: how do you revoke something the server never has to look up? If a user logs out, gets their permissions downgraded, or an admin needs to kill every session for an entire brand, a purely stateless token has no way to know it's no longer valid until it naturally expires.
Solution
Canva's gateways store everything needed to identify a session — user ID, permissions, roles — directly in an encrypted browser cookie. Because the cookie is encrypted (not just signed), gateways can trust its contents outright and authorize a request without a network call to a datastore on every request. This removes the shared datastore from the hot path entirely for the common case.
The hard part is revocation, since a self-contained token doesn't naturally support it. Canva's answer is a revocation list distributed to every gateway, kept in memory:
- Revocations are written to a database, then an asynchronous worker continuously scans for revocations that haven't yet been propagated and uploads them as chunks to S3.
- Each gateway loads and holds these revocation chunks in memory rather than querying a database per-request, keeping the check as fast as the rest of the stateless path.
- Revocations aren't just "kill this session" — the system supports multiple revocation types: invalidating cached permission/role data in a cookie without fully logging the user out, and revoking at the granularity of an entire brand (not just one user) in a single operation. This is encoded efficiently by reserving bits for flags and sorting revocations by principal in a flat array, rather than modeling each revocation type as a separate lookup structure.
- Because the in-memory revocation cache has to be rebuilt on every gateway deploy, loading it fast enough not to bottleneck rollouts became its own engineering problem that had to be solved alongside the steady-state design.
What to steal
- Stateless-by-default, revocation-list-as-the-exception is a more general pattern than "session cookies" — anywhere you want to avoid a network round-trip for the common read path (feature flags, entitlements, API keys), a background-propagated, in-memory-cached negative list lets you keep the fast path stateless while still supporting "kill this now."
- Design the revocation format around the operations you actually need, not a generic revoke-by-ID list. Canva's bit-flag + principal-sorted flat array supports "downgrade permissions without full logout" and "revoke an entire brand" as first-class, cheap operations — worth asking "what are all the granularities we'll ever need to revoke at" before picking a data structure.
- Propagation lag is a deliberate, bounded tradeoff, not an oversight. The async worker + S3 chunk distribution means revocation isn't instant across every gateway — that's an explicit choice to keep the hot path cheap, and it's the kind of tradeoff worth stating out loud (and bounding with an SLA) rather than leaving implicit.
Engineering Lens
This is a clean case of a tradeoff that looks like a contradiction until you separate the read path from the write path: "stateless for speed" and "revocable for security" are usually presented as opposites, but Canva gets both by making revocation an eventually-consistent negative cache layered on top of an otherwise fully stateless token, rather than making every read pay for revocability. In an architecture review, the sharp question to ask about any design like this is "what's the actual propagation bound on revocation, and is that bound acceptable for the sensitivity of what's being revoked?" — a stolen laptop session and a permissions downgrade probably have different acceptable lag, and a mature design would say so explicitly rather than treating "eventually" as good enough everywhere. The bit-flag/principal-sorted revocation array is also a good small example of Principal-level thinking: it's not a fancier algorithm, it's just refusing to build a generic structure when you already know the finite set of operations you need to support cheaply.
Related
- Mutual TLS (mTLS)
- Zero Trust Architecture
- Stripe Connect: Fund Segregation with Allocated Balances
- Companion source: Session revocations at scale (Canva Engineering Blog)