- Home
- Blog
- Ruby & Rails Core
- Rails 8.2 Concurrency: Why Your App Is “Thread-Safe” but Still Slow
Rails 8.2 Concurrency: Why Your App Is “Thread-Safe” but Still Slow
How Ruby 4.0 and Rails 8.2 Expose Connection Pool Starvation in Production
Rails apps don’t usually fail loudly under concurrency. They slow down first.
You upgrade to Rails 8.2, move to Ruby 4.0, enable more threads, and everything looks healthy. CPU is fine. Memory is stable. No errors. And yet response times creep up, background jobs lag, and P99 turns ugly.
This isn’t a bug. It’s a misunderstanding of what Rails 8.2 actually improved — and what it didn’t.
Let’s break down why “thread-safe” doesn’t mean “fast,” where the naïve mental model fails, and how to design concurrency that survives real production load.
The Wrong Mental Model: “Rails 8.2 Fixed Concurrency”
Rails 8.2 made meaningful improvements around safety: clearer locking semantics, better async loading primitives, and tighter guarantees around database access.
What it did not do is remove contention.
Most teams hear “better concurrency” and assume higher throughput. In practice, Rails 8.2 often increases correctness under contention, which can reduce throughput if your app was already operating near pool limits.
I’ve seen apps where average latency stayed flat after upgrading — but P99 doubled. The system didn’t crash. It just queued quietly.
Concurrency failures in Rails are usually wait-time problems, not CPU problems.
Connection Pool Pressure Is the Real Bottleneck
Rails 8.2 encourages more parallelism: async queries, background preloading, and safer transactional boundaries.
All of that competes for the same finite resource: database connections.
Here’s a common setup:
- Puma: 10 threads
- Sidekiq: 10 threads
- Active Record pool: 10 connections
That’s already a deadlock waiting to happen.
Rails 8.2 makes this worse by holding connections slightly longer in more code paths. Under load, threads don’t block on CPU — they block waiting for a connection.
You won’t see this in logs. You’ll see it in response time.
Measure it explicitly:
ActiveSupport::Notifications.subscribe("checkout_active_record") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
Rails.logger.warn("DB wait: #{event.duration}ms") if event.duration > 50
end
If you’re not measuring wait time, you’re debugging blind.
Async Query Loading Increases Contention
Rails 8.2’s async query loading looks like free performance:
users = User.load_async.where(active: true)
In isolation, it helps. Under load, it often hurts.
Async queries check out connections earlier and hold them longer. If your pool is already tight, async loading increases starvation — especially when combined with Ruby 4.0’s more predictable fiber scheduling.
I’ve seen apps where enabling load_async reduced single-request latency but cut overall throughput by 20% at peak.
Async queries help latency. They can destroy throughput.
Use them surgically, not globally.
Background Jobs Compete With Web Requests
Rails 8.2 didn’t change this — but Ruby 4.0 makes it more obvious.
Sidekiq threads are just Ruby threads. They use the same connection pool unless you isolate them.
Under load, jobs that used to “eventually run” now starve web traffic. Or worse, web traffic starves jobs that hold locks longer than expected.
The fix is separation:
- Separate database pools for web and jobs
- Lower Sidekiq concurrency during peak web hours
- Avoid long transactions inside jobs
Example configuration:
production:
pool: <%= ENV.fetch("WEB_DB_POOL", 15) %>
And a separate pool for workers.
Isolation beats tuning every time.
Ruby 4.0 Removes Execution Slack
Ruby 4.0 doesn’t make Rails magically faster. It makes scheduling more honest.
Fiber-aware scheduling and stricter execution semantics mean threads yield less “by accident.” That exposes contention sooner.
In practice, this means:
- Starvation appears at lower traffic levels
- Pool misconfiguration hurts faster
- “It worked before” stops being true
This isn’t a regression. It’s clarity.
Ruby 4.0 turns concurrency bugs into visible performance problems.
A Production-Safe Concurrency Model for Rails 8.2
What actually works at scale:
- Size pools based on total concurrent threads, not Puma alone
- Keep pool utilization under 70% at peak
- Avoid async queries on endpoints that already saturate the pool
- Move non-critical DB work out of request cycles
A simple rule:
If a request waits on the database, adding threads makes it worse.
Rails 8.2 rewards restraint. Ruby 4.0 punishes wishful thinking.
Final Thoughts
Rails 8.2 is thread-safe. That doesn’t mean it’s fast by default.
Combined with Ruby 4.0, it exposes connection starvation, pool misconfiguration, and hidden contention that older stacks masked. Fixing this isn’t about tuning Puma — it’s about respecting the database as the bottleneck.
Concurrency only helps when the system underneath can keep up. If you’re new to orm, start with Rails 8.2 Active Record Changes That Break at Scale.
Was this article helpful?
Your feedback helps us improve our content
How We Verify Conversions
Every conversion shown on this site follows a strict verification process to ensure correctness:
- Compare results on same dataset — We run both SQL and ActiveRecord against identical test data and verify results match
-
Check generated SQL with
to_sql— We inspect the actual SQL Rails generates to catch semantic differences (INNER vs LEFT JOIN, WHERE vs ON, etc.) - Add regression tests for tricky cases — Edge cases like NOT EXISTS, anti-joins, and predicate placement are tested with multiple scenarios
- Tested on Rails 8.1.1 — All conversions verified on current Rails version to ensure compatibility
Last updated: February 22, 2026
Try These Queries in Our Converter
See the SQL examples from this article converted to ActiveRecord—and compare the SQL Rails actually generates.
Deep Dive into ActiveRecord
Raza Hussain
Full-stack developer specializing in Ruby on Rails, React, and modern JavaScript. 15+ years upgrading and maintaining production Rails apps. Led Rails 4/5 → 7 upgrades with 40% performance gains, migrated apps from Heroku to Render cutting costs by 35%, and built systems for StatusGator, CryptoZombies, and others. Available for Rails upgrades, performance work, and cloud migrations.
More on SQL to ActiveRecord
Junior Dev Asked "Why Not Just Use SQL?" Gave Textbook Answer. They Weren't Convinced. Then Production Happened.
Not ideology—operations. See how ActiveRecord cut P95 from 1.2s→220ms, dropped queries 501→7, and avoided schema-change bugs. When to use SQL safely, too.
SQL Certification on Resume. Rails Interview Failed. Knew Databases. Didn't Know ActiveRecord.
SQL cert on your resume but Rails interview still flopped? Learn the ActiveRecord skills interviews test—associations, eager loading, batching, and when to use raw SQL.
Read "Agile Web Development with Rails." Still Couldn't Write Queries. Needed Examples, Not Theory.
Books teach concepts. You need examples. See SQL vs ActiveRecord side-by-side, when to use scopes/Arel/SQL, and how to ship maintainable queries fast.
Leave a Response
Responses (0)
No responses yet
Be the first to share your thoughts