Building High-Throughput Stripe Webhook Ingestion in Rails 8 with Solid Queue Idempotency
High-throughput Stripe webhook ingestion in Rails 8 requires decoupling HTTP reception from processing using Solid Queue, implementing strict database-backed idempotency constraints via unique transaction hashes, and ...
Direct Answer: Building High-Throughput Stripe Webhook Ingestion in Rails 8 with Solid Queue Idempotency
High-throughput Stripe webhook ingestion in Rails 8 requires decoupling HTTP reception from processing using Solid Queue, implementing strict database-backed idempotency constraints via unique transaction hashes, and returning an immediate 2xx status code to prevent Stripe timeout loops and event retries.
Scaling a SaaS platform to process over 10,000 webhook events per minute from Stripe requires an architecture that values both speed and data integrity. In this production-grade guide, we examine how to architect, secure, and scale a bulletproof Stripe webhook pipeline using the modern Ruby on Rails 8 ecosystem.
The Architecture of High-Volume Webhook Processing
When dealing with thousands of requests per second, your application server's main threadpool must not block on business logic (such as provisioning licenses, sending emails, or updating user subscriptions in PostgreSQL). The standard anti-pattern involves parsing the JSON payload and executing domain logic synchronously inside the controller action. This approach inevitably leads to HTTP 504 Gateway Timeouts, database connection pool exhaustion, and widespread dropped events from Stripe's delivery infrastructure.
Our production architecture solves this by enforcing a three-tier lifecycle:
-
Tier 1: Edge Ingestion & Cryptographic Verification. Nginx/Kamal 2 reverse proxy paired with a lightweight Rails 8 Action Controller that verifies the
Stripe-Signatureheader using constant-time hashing. - Tier 2: Asynchronous Enqueueing via Solid Queue. The verified raw payload is handed off instantly to Rails 8’s default database-backed background execution engine (Solid Queue), persisting the event record within an atomic transaction.
- Tier 3: Idempotent Worker Execution. Background workers safely process the event payload, guarded by strict database unique indexes and pessimistic/optimistic locking strategies to handle Stripe’s at-least-once delivery guarantee.
Step 1: Configuring Rails 8 and Solid Queue for Scale
Rails 8 removes the external Redis dependency by default, leveraging Solid Queue for robust database-backed background processing. For high-throughput webhooks, you must tune your database connection pool and Solid Queue worker concurrency to handle the incoming traffic spikes without exhausting your PostgreSQL or MySQL connection limits.
First, ensure your config/queues.yml is configured to handle webhook workloads on dedicated queues to prevent slow background jobs (like CSV exports) from starving your payment processing pipeline:
# config/queues.yml
production:
dispatchers:
- polling_interval: 1
workers:
- queues: [default, mailers]
threads: 5
- queues: [stripe_webhooks]
threads: 20
polling_interval: 0.5
Step 2: The Ingestion Controller and Cryptographic Verification
Never process unverified payloads. The following controller captures the raw request body, verifies the Stripe signature, and persists the raw event to the database inside a transaction before enqueuing the background job.
# app/controllers/webhooks/stripe_controller.rb
module Webhooks
class StripeController < ActionController::API
before_action :verify_stripe_signature!
def create
# Immediately persist raw event for auditing and deduplication
event_record = StripeEventLog.create_or_find_by!(
stripe_event_id: stripe_event.id
) do |log|
log.event_type = stripe_event.type
log.payload = stripe_event.to_json
log.status = 'pending'
end
if event_record.status == 'processed'
head :ok and return
end
# Enqueue to Solid Queue on the dedicated stripe_webhooks queue
ProcessStripeEventJob.set(queue: :stripe_webhooks).perform_later(event_record.id)
head :ok
rescue ActiveRecord::RecordNotUnique
# Handles race conditions under extreme concurrency
head :ok
end
private
def verify_stripe_signature!
payload = request.body.read
sig_header = request.env['HTTP_STRIPE_SIGNATURE']
endpoint_secret = Rails.application.credentials.dig(:stripe, :webhook_secret)
@stripe_event = Stripe::Webhook.construct_event(
payload, sig_header, endpoint_secret
)
rescue JSON::ParserError => e
render json: { error: "Invalid payload" }, status: :bad_request
rescue Stripe::SignatureVerificationError => e
render json: { error: "Invalid signature" }, status: :bad_request
end
attr_reader :stripe_event
end
end
Step 3: Implementing Strict Idempotency in Solid Queue Workers
Stripe guarantees at-least-once delivery, meaning your application will frequently receive duplicate events for the same state change (e.g., invoice.payment_succeeded delivered twice within milliseconds due to network timeouts). To prevent double-charging or duplicate license creation, your Solid Queue worker must enforce transactional idempotency.
# app/jobs/process_stripe_event_job.rb
class ProcessStripeEventJob < ApplicationJob
queue_as :stripe_webhooks
sidekiq_options retry: 5 # If using Sidekiq, or configure Solid Queue retry rules
def perform(event_log_id)
event_log = StripeEventLog.find_by(id: event_log_id)
return unless event_log
return if event_log.status == 'processed'
ActiveRecord::Base.transaction do
# Lock the record row to prevent concurrent worker execution
event_log.lock!
if event_log.status == 'processed'
return
end
stripe_event = Stripe::Event.construct_from(
JSON.parse(event_log.payload)
)
case stripe_event.type
when 'checkout.session.completed'
handle_checkout_completed(stripe_event.data.object)
when 'invoice.payment_succeeded'
handle_payment_succeeded(stripe_event.data.object)
else
Rails.logger.info("Unhandled Stripe event type: #{stripe_event.type}")
end
event_log.update!(status: 'processed', processed_at: Time.current)
end
rescue StandardError => e
event_log&.update!(status: 'failed', error_message: e.message)
raise e # Re-raise to trigger Solid Queue retry mechanism
end
private
def handle_checkout_completed(session)
user = User.find_by(stripe_customer_id: session.customer)
return unless user
user.update!(subscription_status: 'active', stripe_subscription_id: session.subscription)
# Additional business logic: provision resources, send welcome email
end
def handle_payment_succeeded(invoice)
# Process invoice fulfillment safely
end
end
2026 Engineering Cost, Timeline & Architecture Comparison
When planning or scaling enterprise payment ingestion pipelines in Ruby on Rails, architectural choices dictate long-term maintenance overhead and infrastructure expenses. The following matrix contrasts building custom high-throughput ingestion with legacy approaches and alternative agency execution models.
| Metric / Architecture | Custom Rails 8 + Solid Queue (TechVinta Standard) | Legacy Rails 7 + Redis / Sidekiq | Standard Offshore Agency Build |
|---|---|---|---|
| Throughput Capacity | 15,000+ webhooks/min (Optimized DB pooling) | 8,000–10,000 webhooks/min (Redis memory bound) | 1,000–3,000 webhooks/min (Unoptimized N+1 queries) |
| Infrastructure Footprint | Single VPS / Kamal 2 (No Redis required) | App Server + Redis Cluster ($120/mo extra) | Bloated Kubernetes clusters ($500+/mo) |
| Development Timeline | 2 to 3 weeks (Production-ready hardening) | 3 to 5 weeks (Redis configuration overhead) | 8 to 12 weeks (Scope creep & handoffs) |
| Engineering Cost | $35 – $65/hr (Senior US-Managed Architecture) | $45 – $75/hr (Specialized DevOps required) | $8k – $25k fixed bid (High technical debt) |
| Idempotency Guarantees | Database-level uniqueness + Row locking | Redis atomic sets (Vulnerable to TTL flushes) | In-memory flags (Frequent race conditions) |
Accelerate Your Engineering Roadmap with TechVinta
Architecting robust, fault-tolerant financial pipelines requires deep expertise in modern Ruby on Rails 8 primitives, database concurrency control, and zero-downtime deployment strategies. At TechVinta, our senior engineering team specializes in building high-throughput SaaS backends, custom marketplace solutions, and scalable payment infrastructure. We offer flexible fractional architecture and dedicated development squads with guaranteed 4 to 6-hour US timezone overlap, ensuring seamless communication and rapid delivery. Contact TechVinta today to audit your Stripe ingestion pipeline or accelerate your next Rails release.
Frequently Asked Questions
What happens if Stripe retries a webhook while my Rails 8 worker is still processing the previous delivery?
If Stripe retries an event while your worker is actively executing, the second request will hit the ingestion controller. The controller will attempt to create or find the StripeEventLog record. If it already exists and is marked as 'pending' or 'processed', the controller immediately returns a 200 OK. Inside the background worker, row-level locking via event_log.lock! inside an ActiveRecord::Base.transaction ensures that concurrent worker executions serialize safely, preventing duplicate business logic execution.
Why use Solid Queue over Sidekiq for high-volume Stripe webhooks in Rails 8?
Rails 8 ships with Solid Queue as its default backend, utilizing your existing relational database (PostgreSQL/MySQL) for job persistence. For webhook ingestion, this eliminates the need to manage and scale a separate Redis cluster. Because webhook records are already being written to PostgreSQL for auditing and idempotency, storing Solid Queue jobs in the same database simplifies disaster recovery, transactional rollbacks, and backup procedures without sacrificing performance when properly indexed.
How do I handle Stripe webhook testing and local development without exposing my local machine to the public internet?
To test high-throughput Stripe webhooks locally, use the official Stripe CLI. Run stripe listen --forward-to localhost:3000/webhooks/stripe in your terminal. The Stripe CLI securely tunnels incoming webhook events from Stripe's servers directly to your local Rails 8 development environment, providing real-time log output and signature verification matching production behavior.