ruby yaml 70 lines · 4 tabs

“Read Your Writes” Consistency: Pin to Primary After POST

Shared by codesnips Jan 2026
4 tabs
module Middleware
  class DatabasePinning
    PIN_WINDOW = 5.seconds

    def initialize(app)
      @app = app
    end

    def call(env)
      request = ActionDispatch::Request.new(env)
      last_write = request.session[:last_write_at]

      if pin_to_primary?(last_write)
        ActiveRecord::Base.connected_to(role: :writing) do
          @app.call(env)
        end
      else
        @app.call(env)
      end
    end

    private

    def pin_to_primary?(last_write)
      return false if last_write.blank?

      Time.zone.at(last_write) > PIN_WINDOW.ago
    rescue ArgumentError, TypeError
      false
    end
  end
end
4 files · ruby, yaml Explain with highlit

Read replicas absorb read traffic, but they lag behind the primary by milliseconds to seconds. That lag breaks a very common expectation: after a user submits a form (a POST), the immediate redirect and follow-up GET should reflect what was just written. If that GET is served from a stale replica, the user sees their edit vanish — the classic "read your writes" violation. This snippet implements session-based primary pinning so that reads following a recent write are routed to the primary for a short window.

In DatabasePinning middleware, the Rack middleware reads a last_write_at timestamp stored in the encrypted session cookie. If a write happened within PIN_WINDOW, it wraps the request in ActiveRecord::Base.connected_to(role: :writing) so every query in that request hits the primary. Otherwise it lets Rails' automatic role switching route reads to a replica. The window is deliberately short (a few seconds) — just long enough to cover the redirect-and-render cycle while replication catches up, so the vast majority of read traffic still benefits from replicas.

The write side lives in ApplicationController. The after_action callback mark_write inspects request.request_method and stamps session[:last_write_at] after any non-idempotent verb (POST, PATCH, PUT, DELETE). Storing the marker in the session ties pinning to a single user rather than pinning the whole app, which would defeat the purpose of having replicas. Because the value lives in the signed/encrypted cookie, it survives the redirect without any server-side state.

config/database.yml shows the two-connection setup: a primary pointing at the writer endpoint and a primary_replica marked replica: true sharing the same migrations_paths. This is what lets connects_to database: { writing: :primary, reading: :primary_replica } and automatic role switching work.

The main trade-off is that pinning is time-based, not replication-aware: it assumes lag is under PIN_WINDOW. Under heavy replication delay a stale read can still slip through, so the window should be tuned to the observed p99 lag. A pitfall to watch is background jobs and async work spawned inside a request — they run outside the middleware and won't inherit the pin, so any read-your-writes guarantees there must be enforced explicitly with connected_to.


Related snips

Share this code

Here's the card — post it anywhere.

“Read Your Writes” Consistency: Pin to Primary After POST — share card
Link copied