Shared Quotas and Sessions
Coordinate route quotas and revocable sessions across workers with optional Redis or Valkey storage.
Configure the shared backend
Section titled “Configure the shared backend”Install the optional storage dependency:
uv add flasgo --extra redisThe optional extra is named redis for both Redis and Valkey. Shared storage requires Flasgo 0.9.0 or newer; see version checks and upgrades if your project already uses an older Flasgo release.
import osfrom flasgo import Flasgo, RedisRateLimiter, RedisStore, ServerSideSessions
store = RedisStore.from_url(os.environ["FLASGO_REDIS_URL"], namespace="billing")app = Flasgo( settings={"SECRET_KEY": os.environ["FLASGO_SECRET_KEY"]}, rate_limiter=RedisRateLimiter(store), session_backend=ServerSideSessions(store),)
@app.lifespanasync def lifespan(app: Flasgo): try: yield finally: await store.aclose()Use a TLS rediss:// URL and appropriate credentials for a remote service. Redis and Valkey use the same adapter.
Connection acquisition and operations have finite timeouts; the convenience constructor caps the pool at 100
connections. Both RedisStore(...) and RedisStore.from_url(...) accept max_value_bytes=65536 to bound each
stored value. RedisStore(client, ...) can wrap an application-owned async Redis-compatible client, whose lifecycle
remains application-owned. aclose() closes only a client created by from_url. Lua scripts run with EVALSHA,
falling back to EVAL when the server replies NOSCRIPT, so the script body is not resent on every request. A wrapped
client must provide both evalsha and eval.
Give each application a distinct stable namespace and use the same namespace and signing secret across its workers.
A namespace’s keys share a cluster hash slot to keep multi-key Lua operations atomic; this deliberately trades
cross-slot scaling for consistent accounting. Use a dedicated, authenticated store with noeviction, suitable
persistence/availability controls, and restrictive access. If a server evicts or loses security state, an application
cannot reconstruct previous quotas. Never use a cache configured to discard live keys for this purpose.
Shared route quotas
Section titled “Shared route quotas”RedisRateLimiter implements shared sliding windows with server time, atomic evaluation of all rules in one phase,
and a bounded number of active quota keys (max_keys=10000). Capacity pressure rejects new keys without evicting
existing quotas; Retry-After then reports when the earliest active quota key expires. It supports up to 64 rules per
phase. Its default identities use protocol, route path/methods, and rule position, never process-local object IDs.
Explicit scope= shares a quota across routes; when a shared scope denies a request, Retry-After waits until enough
entries expire to fall back under that rule’s limit. HTTP methods belonging to separate route registrations have
separate default quotas; GET/HEAD in the same registration share one. Keep rule configuration consistent across
workers. The existing in-process limiter retains its callable-based behavior. Authentication-failure throttling and
per-WebSocket message throttling remain process-local; shared storage here coordinates decorated route quotas. Keep
edge protections for aggregate abuse control.
Storage errors return 503, without falling back to independent per-worker quotas or accepting unauthenticated
session state. A distributed timeout may occur after the server applied an operation; retryable callers must account
for that uncertainty. Quotas are abuse controls, not an exactly-once application transaction mechanism.
Revocable server-side sessions
Section titled “Revocable server-side sessions”ServerSideSessions stores bounded JSON behind a random 256-bit opaque cookie identifier. Session.regenerate()
rotates the identifier on the next save; call it after login or privilege changes. Session.clear() deletes the
session when saved. await backend.revoke(session_id) supports explicit revocation; treat Session.session_id as a
credential and never log it. Session metadata is private to response serialization.
Updates compare the previously loaded stored value before replacing it. Rotation atomically replaces the identifier.
A stale concurrent write, expired session, or revoked session produces 409 rather than recreating old authenticated
state. Applications must not blindly replay side effects after a session conflict: arrange session changes before
irreversible work or use application transactions/idempotency as appropriate. Expiry uses SESSION_COOKIE_MAX_AGE;
modified saves refresh the storage TTL. Revocation affects subsequent loads, not requests/connections already using
an acquired identity. WebSocket handlers do not persist session changes.
Failed dispatch does not save session mutations. A successful custom error handler receives the restored session and may deliberately clear or change it. Response validation also precedes session writes. An explicitly returned error status still permits normal persistence, and business side effects are not rolled back. See Session Changes and Failures.
Duplicate session cookies are treated as an absent credential without loading either identifier from storage. WebSocket head, Host, Origin, and default client-IP route checks run before session loading.
Migration and storage failures
Section titled “Migration and storage failures”Signed-cookie sessions remain the default. Switching an existing deployment to server-side sessions intentionally
requires users with old signed cookies to log in again; no implicit cookie-format migration is attempted. CSRF remains
bound to the actual session cookie, including identifier rotation. MemoryStore supplies bounded process-local
storage for development/tests. New sessions are created only when modified, with a default maximum serialized size
of 64 KiB. Monitor storage availability and response-preparation failures through structured events. StoreUnavailable is the backend exception type.
Development storage and integration tests
Section titled “Development storage and integration tests”For process-local session tests, configure session_backend=ServerSideSessions(MemoryStore()). MemoryStore(max_keys=10000) is bounded and does not coordinate workers. It is a session store, not a Redis quota emulator.
The framework’s shared-storage integration tests use FLASGO_TEST_REDIS_URL when set. Otherwise they launch an installed redis-server or valkey-server over an isolated temporary Unix socket; these tests are skipped if neither a server nor a URL is available. Use a dedicated test service and namespace. CI supplies pinned Redis and Valkey services.
Test quota sharing, outage responses, expiry, revocation, identifier rotation, concurrent session conflicts, and CSRF binding. See Testing and Security and Authentication.