Skip to content

PATTERN Cited by 1 source

Session cookie for read-your-writes

Pattern

When the database architecture is read-write split (writes to primary, reads to replica pool) and the replica pool is eventually consistent, preserve read-your-writes consistency by setting a per-user session cookie on every write and routing reads to primary for Δ seconds while that cookie is fresh.

"we can also tell Rails to read from our primary if the user recently wrote to the database. This protects our users from ever reading stale data due to replication lag … After each write, it will set a cookie that will send all reads to the primary for 2 seconds, allowing users to read their own writes." (Source: sources/2026-04-21-planetscale-introducing-planetscale-portals-read-only-regions.)

The concept sibling — concepts/session-cookie-read-your-writes-window — covers the properties of the window itself; this pattern page is the applied recipe.

When to apply

  • Architecture already has read-write splitting. This pattern is the RYW-preserving companion to patterns/read-replicas-for-read-scaling or patterns/per-region-read-replica-routing.
  • User-facing interactive app. RYW is a perceived-correctness property a human user notices. Agent-to-agent / server-to-server traffic usually doesn't care.
  • Cookies are in the request path anyway. Rails, Django, Laravel, Express-with-sessions, etc. — anywhere a per-user session state is already threaded through every request. For stateless-token auth (JWT-only APIs), the same mechanism works but the pin must live in the user's client state (localStorage, header) and be echoed back, which is more plumbing.

Mechanics

  1. Choose Δ. Must exceed p99 replication lag across the replica pool. For same-region replicas, 1–2 s suffices. For regional read replicas with cross-region lag, 5–10 s is safer. Graph Δ vs. replica-lag p99 to verify.
  2. On every write (POST/PATCH/DELETE in HTTP terms, or any transaction that commits changes), set a cookie with the current timestamp.
  3. On every read, check the cookie. If now() - t_cookie < Δ, route the read to primary. Else route to the replica pool.
  4. Surface a telemetry flag — per-request, was this request routed to primary because of RYW, or because of replica- selection policy? This lets you see the RYW-tax rate (% of reads forced to primary) and debug the mechanism in production.

Rails-specific invocation

# config/environments/production.rb
config.active_record.database_selector     = { delay: 2.seconds }
config.active_record.database_resolver     = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
  connects_to database: { writing: :primary, reading: :primary_replica }
end

The DatabaseSelector middleware handles cookie set on writes, cookie read on reads, and routing. Default delay is 2 seconds; pick a larger value if your replica pool is geographically distributed.

Django-specific invocation

# settings.py
DATABASES = {'default': {...}, 'replica': {...}}
DATABASE_ROUTERS = ['app.routers.RYWRouter']

# app/routers.py
class RYWRouter:
    def db_for_read(self, model, **hints):
        req = hints.get('request')
        if req and _cookie_fresh(req, delta=timedelta(seconds=2)):
            return 'default'
        return 'replica'
    def db_for_write(self, model, **hints):
        req = hints.get('request')
        if req: _set_cookie(req)
        return 'default'

Laravel-specific note

Laravel's built-in 'sticky' => true read/write config is a weaker form: sticky reads stay on the write connection for the rest of the current request, not across subsequent requests in the same session. For true RYW across page reloads, add a session cookie check in addition to the sticky flag.

Trade-offs

Dimension Effect
RYW correctness ✓ (within Δ)
In-region read latency for the writing user paid primary-region RTT for Δ seconds (possibly cross-region)
In-region read latency for other users unchanged — they read from replica
Plumbing cost ~20 lines of middleware
Granularity coarse — all reads for the user pin, not just reads that depend on the write
Failure mode if Δ too small ghost-reads during replication stalls
Failure mode if Δ too large unnecessary primary reads — more load on primary, more in-region RTT for the user

Why not a token-based scheme instead?

Token-based RYW (LSN, GTID, MongoDB $clusterTime) is more precise — only reads that actually depend on the write pay the primary-read tax, and the check is "has replica R caught up to LSN X?" rather than a coarse time window. But it requires:

  • Write returns an opaque position token to the client.
  • Every subsequent read sends the token.
  • Router checks each replica's current position against the token before routing — or forwards to primary if no replica is caught up.

For most interactive web apps, the session-cookie coarseness is fine and the token plumbing isn't worth it. Choose tokens for agent pipelines, high-QPS read paths, or platforms that already surface LSN / GTID cheaply.

Seen in

Last updated · 378 distilled / 1,213 read