class LastSeenTracker
THROTTLE = 5.minutes
PENDING_KEY = "pending:last_seen".freeze
class << self
def touch(user_id, at: Time.current)
marker = "seen:mark:#{user_id}"
# NX + EX means "only proceed once per THROTTLE window per user".
return false unless redis.set(marker, 1, nx: true, ex: THROTTLE.to_i)
redis.hset(PENDING_KEY, user_id, at.utc.iso8601)
true
end
def current_for(user_id)
raw = redis.hget(PENDING_KEY, user_id)
raw && Time.iso8601(raw)
end
def drain
pending = redis.hgetall(PENDING_KEY)
return {} if pending.empty?
redis.del(PENDING_KEY)
pending.transform_values { |v| Time.iso8601(v) }
end
private
def redis
RedisPool.current
end
end
end
class LastSeenFlushJob < ApplicationJob
queue_as :low
def perform
pending = LastSeenTracker.drain
return if pending.empty?
rows = pending.map do |user_id, seen_at|
{ id: user_id.to_i, last_seen_at: seen_at }
end
rows.each_slice(1_000) do |batch|
User.upsert_all(
batch,
unique_by: :id,
update_only: [:last_seen_at]
)
end
end
end
class ApplicationController < ActionController::Base
before_action :track_last_seen
private
def track_last_seen
return unless current_user
# Cheap: usually a single Redis command, no DB write on the hot path.
LastSeenTracker.touch(current_user.id)
rescue Redis::BaseError => e
# Presence is best-effort; never fail a request over it.
Rails.logger.warn("last_seen skipped: #{e.message}")
end
end
Tracking when a user was "last seen" is deceptively expensive. A naive implementation updates users.last_seen_at on every request, which turns a single row into a write hotspot: each UPDATE produces a new row version, bloats the table, thrashes indexes, and — on a chatty user — serializes behind row locks. The value barely matters to the second, so paying full transactional write cost for it is wasteful.
This snippet shows a two-layer throttle that keeps the per-request path cheap and coalesces writes into infrequent batches. In LastSeenTracker, touch records a timestamp in Redis using SET with an EX expiry, but only performs the write once per THROTTLE window per user thanks to the NX guard on a separate marker key. That makes the common case a single, in-memory Redis command with no database contact at all. The pending timestamps accumulate in a Redis hash (pending:last_seen) keyed by user id, so the source of truth for the not-yet-persisted value lives outside Postgres.
The LastSeenFlushJob is what eventually moves data into the database, and it is where the hot-row problem is actually solved. Instead of one UPDATE per user, it drains the pending hash and issues a single upsert_all (with HGETDEL-style read-then-clear semantics via hgetall + del) so that dozens or thousands of updates collapse into one bulk statement. The unique_by option lets Postgres apply the batch as an ON CONFLICT upsert against the primary key, and update_only restricts the write to the last_seen_at column so unrelated attributes are never touched.
The ApplicationController wires it in through a before_action that calls LastSeenTracker.touch, keeping controllers ignorant of the throttling mechanics. A scheduled trigger (cron, Sidekiq-cron, etc.) enqueues LastSeenFlushJob on an interval.
The trade-off is deliberate: last_seen_at becomes eventually consistent and can lag by up to the flush interval, and a Redis outage can lose the most recent unflushed timestamps. For presence and analytics that is almost always acceptable, and in exchange the database is spared enormous write amplification. The main pitfall is forgetting that the Redis value is fresher than the column, so any read that needs real-time accuracy should consult LastSeenTracker, not the row.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.