- Home
- Blog
- Ruby & Rails Core
- Ruby 3.5 Preview: What Matters for Rails in Production
Ruby 3.5 Preview: What Matters for Rails in Production
Ruby upgrades help, but your slow endpoints are still slow for the same reasons: query shape, object allocations, and cache pressure.
Ruby 3.5 was released as a preview; Ruby 4.0 shipped. Here’s what actually matters for Rails apps: what won’t change, what can break, and where SQL/ActiveRecord fixes beat runtime upgrades.
Ruby 3.5 Game Changers for Rails Apps
Ruby upgrades help, but your slow endpoints are still slow for the same reasons: query shape, object allocations, and cache pressure.
Start with a real production bug
We upgraded Ruby expecting our slowest admin endpoint to “just get faster”.
It didn’t.
The endpoint was a classic Rails anti-pattern: it loaded thousands of ActiveRecord objects, selected *, and then did Ruby-side work that should have been pushed down into SQL.
The runtime improved. The endpoint stayed slow.
The wrong mental model
“If Ruby gets faster, my Rails app gets faster.”
Ruby upgrades can improve:
- object allocation costs
- JIT quality on CPU-heavy code paths
- GC behavior in certain workloads
But most Rails app latency isn’t CPU-bound Ruby. It’s:
- database time
- object instantiation + memory churn
- N+1s and oversized result sets
So if your app is slow because you’re pulling too much data and allocating too many objects, a faster runtime doesn’t change the shape of the problem.
First: reality check on Ruby 3.5
Ruby 3.5 was released as 3.5.0-preview1 (a preview release). Ruby 4.0.0 was released later as the stable yearly release.
So if your blog post is written as if Ruby 3.5 is already stable and “transforms” Rails apps, it will age badly.
The productive framing is:
- Preview releases: good for testing compatibility and measuring trends
- Stable releases: what you actually upgrade production to
The naive approach (and why it fails)
Here’s the pattern that shows up everywhere:
# app/controllers/admin/orders_controller.rb
orders = Order
.where(status: "open")
.order(created_at: :desc)
.limit(5_000)
orders.each do |order|
# Ruby-side work
end
Generated SQL (verified):
SELECT "orders".*
FROM "orders"
WHERE "orders"."status" = 'open'
ORDER BY "orders"."created_at" DESC
LIMIT 5000;
Why this stays slow even on a faster Ruby
-
SELECT *pulls columns you don’t need - ActiveRecord instantiates thousands of Ruby objects
- allocations + GC dominate the request
- the DB may still be doing extra work if you don’t have the right index
A runtime upgrade can shave some CPU time, but it won’t:
- remove object creation
- remove
SELECT * - remove missing indexes
- reduce network I/O
The correct approach: fix the query shape first
1) Select only what you need
If you need a subset of fields:
orders = Order
.where(status: "open")
.select(:id, :total_cents, :created_at)
.order(created_at: :desc)
.limit(5_000)
Generated SQL (verified):
SELECT "orders"."id", "orders"."total_cents", "orders"."created_at"
FROM "orders"
WHERE "orders"."status" = 'open'
ORDER BY "orders"."created_at" DESC
LIMIT 5000;
This reduces:
- row size transferred from Postgres
- Ruby object size
- overall memory churn
2) If you don’t need models, don’t instantiate models
If you’re building a report or exporting rows, prefer pluck:
rows = Order
.where(status: "open")
.order(created_at: :desc)
.limit(5_000)
.pluck(:id, :total_cents, :created_at)
Generated SQL (verified):
SELECT "orders"."id", "orders"."total_cents", "orders"."created_at"
FROM "orders"
WHERE "orders"."status" = 'open'
ORDER BY "orders"."created_at" DESC
LIMIT 5000;
This is a “Ruby upgrade proof” optimization: it helps on every Ruby version.
3) Add the index that matches the query
If you frequently do:
WHERE status = 'open'
ORDER BY created_at DESC
LIMIT ...
A practical index is:
CREATE INDEX CONCURRENTLY index_orders_on_status_created_at
ON orders (status, created_at DESC);
This is the kind of change that often beats any runtime win.
Where Ruby improvements still matter
Once you fix query shape, you’ll see where Ruby changes help:
- request middleware overhead
- CPU-heavy JSON rendering
- background jobs doing parsing/transforms
- hot Ruby loops (rare, but real)
At that point, enabling/validating JIT improvements can become meaningful — because you’ve removed the dominant bottleneck.
Edge cases and production pitfalls
-
selectcan break code that assumes missing attributes exist (e.g., callingorder.user_idwhen you didn’t select it). -
pluckbypasses type casting in some paths and returns raw values; treat it as “data extraction”, not a model. - Indexes aren’t free: they cost write amplification and disk. Add them where query frequency justifies it.
- Preview versions are not upgrade targets: treat them as compatibility probes and benchmark baselines.
Rule of thumb
Upgrade Ruby for compatibility and long-term health.
Fix SQL and allocation patterns for speed.
If your Rails app is slow, start with:
- query shape (columns, limits, joins)
- index alignment
- object instantiation (pluck/select/batches)
- only then: runtime/JIT tuning
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: January 16, 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 Ruby & Rails Core
LEFT JOIN + WHERE in ActiveRecord: The Trap That Turns It Into INNER JOIN
LEFT JOIN queries often break the moment you add a WHERE on the joined table—silently turning into INNER JOIN behavior. Learn the correct Rails patterns (ON vs WHERE, scoped associations, where.missing/where.associated, EXISTS) and how to verify generated SQL.
Ruby 4.0.0 Is Out: What Rails & ActiveRecord Devs Actually Need to Know
Ruby 4.0.0 is released (Dec 25, 2025). For Rails apps, the real risks are stdlib gem changes (CGI), Net::HTTP behavior shifts, and Bundler 4. Here’s what breaks first, how to fix safely, and what Ruby 4 performance does—and doesn’t—solve.
N+1 Isn’t a Rails Problem: It’s a Query-Shaping Problem
Copied SQL into Rails and still got N+1? Rewrote in ActiveRecord and still got N+1? The fix is a set-based mental model: where N+1 really comes from, how to verify it, and the AR patterns that eliminate it safely.
Responses (0)
No responses yet
Be the first to share your thoughts