ruby 57 lines · 1 tab

Request deduplication with idempotency keys

Alex Kumar Jan 2026
1 tab
module Idempotency
  extend ActiveSupport::Concern

  included do
    before_action :check_idempotency_key, only: [:create, :update]
    after_action :store_idempotent_response, only: [:create, :update]
  end

  private

  def check_idempotency_key
    return unless idempotency_key.present?

    cached = Rails.cache.read(idempotency_cache_key)
    return unless cached

    request_hash = compute_request_hash

    if cached[:request_hash] != request_hash
      render json: {
        error: 'IDEMPOTENCY_KEY_MISMATCH',
        message: 'Idempotency key was used with different request body'
      }, status: :conflict
      return
    end

    # Return cached response
    render json: cached[:response_body], status: cached[:status]
  end

  def store_idempotent_response
    return unless idempotency_key.present?
    return unless response.successful?

    Rails.cache.write(
      idempotency_cache_key,
      {
        request_hash: compute_request_hash,
        response_body: JSON.parse(response.body),
        status: response.status
      },
      expires_in: 24.hours
    )
  end

  def idempotency_key
    @idempotency_key ||= request.headers['Idempotency-Key']
  end

  def idempotency_cache_key
    "idempotency:#{idempotency_key}"
  end

  def compute_request_hash
    Digest::SHA256.hexdigest(request.raw_post)
  end
end
1 file · ruby Explain with highlit

Network failures and client retries can cause duplicate request processing, leading to duplicate charges, double-created resources, or inconsistent state. Idempotency keys solve this by tracking processed requests and returning cached responses for duplicates. Clients send a unique Idempotency-Key header with each mutating request. The server stores a hash of the request body along with the response in Redis or a database table. If the same key arrives with identical body hash, I return the cached response. If the key exists but the body differs, that indicates a client bug—I return 409 Conflict to signal the problem. Keys should have reasonable TTL (hours to days) to balance deduplication effectiveness with storage costs.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
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

rails activerecord patterns
by Alex Kumar 1 tab
ruby
payload = {
  sub: user.id,
  iss: 'https://auth.example.com',
  aud: 'codesnips-api',
  exp: 15.minutes.from_now.to_i,
  iat: Time.now.to_i,

JWT issuance and verification without common footguns

jwt authentication api
by Kai Nakamura 2 tabs
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
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

rails turbo hotwire
by codesnips 4 tabs

Share this code

Here's the card — post it anywhere.

Request deduplication with idempotency keys — share card
Link copied