The State of Ruby CI 2026
Why Rails suites get slow, what parallel_tests and Bundler caching actually buy back, and the per-minute economics underneath it all.
Executive summary
Ruby CI lives and dies by the test suite. A mature Rails application accumulates thousands of RSpec examples over its lifetime, and the slowest of them, the browser-driven system and feature specs, dominate the clock far out of proportion to their count. Each one boots a real driver, talks to a real database, and waits on real rendering, so a few hundred of them can outweigh several thousand fast unit examples. Layer an uncached Bundler install on top of every run and the pipeline gets slow long before the application itself does.
With CI adoption at 76% among professional developers, the Ruby question in 2026 is not whether to test on every push, it is how to keep a green pipeline under ten minutes as the suite grows. Rails rewards teams that ship small changes often, and a slow pipeline punishes exactly that habit. The teams that stay fast treat suite duration as a budget they defend, and they reach for sharding and caching before they reach for a bigger runner, because most of the cost is structural rather than fundamental.
This report quantifies where Rails CI minutes go. We look at how the spec mix splits a billed minute, what parallel_tests actually buys as cores scale, how much a cold Bundler install costs versus a warm one, and why flaky system specs trigger the most expensive re-runs in the entire pipeline. The throughline is that the bulk of Rails CI waste is mechanical: it comes from recomputing unchanged work and from retrying transient failures, neither of which requires touching a line of application code to fix.
Three numbers frame the year for Ruby teams. A little under three fifths of a typical Rails CI minute is spent inside system and feature specs, which makes them the obvious first target. Sharding with parallel_tests across four cores cuts suite wall-clock by roughly a factor of three, but only until it saturates the cores the runner actually has. And a cold Bundler install for a mature Gemfile lands around a minute, almost all of it pure waste when the dependency set has not changed since the last run.
The encouraging part for Rails leaders is that none of the highest-return levers are exotic. Cache Bundler against the lockfile, shard the suite across enough cores to matter, push assertions down from system specs to request specs where a full browser is not required, and let a self-healing runner absorb the transient browser-driver flakes that would otherwise force a full, expensive re-run. These are pipeline hygiene plus the right runner layer underneath, not a platform-team moonshot.
Modeled split of billed minutes for a mature Rails pipeline. · Source: Latchkey analysis (modeled)
Wall-clock to run the full suite as workers scale. · Source: Latchkey analysis (modeled)
Email me the report
The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.
Sent! Check your inbox for the report link.
No spam. Unsubscribe anytime.
System specs are slow out of all proportion to their count
Browser-driven system and feature specs are a small fraction of the examples in a Rails suite and a large fraction of its wall-clock. The reason is structural: each one boots a real browser driver, exercises the full request cycle, hits the database, and then waits on JavaScript and rendering before it can assert anything. A unit example finishes in milliseconds; a system spec routinely takes seconds. A few hundred of the latter can outweigh several thousand of the former on the clock.
Because they are the slowest tests, system specs also set the ceiling on how fast the suite can possibly go. Sharding helps, but if a single shard inherits a cluster of heavy system specs, that shard becomes the long pole and the whole pipeline waits on it. This is why the spec mix, and not just the raw example count, is what teams need to look at when a suite starts creeping past the ten-minute mark.
The highest-return move for most Rails teams is to audit the slowest ten percent of specs and ask, for each one, whether it genuinely needs a browser. A large share of system specs are really asserting controller behavior or JSON responses that a request spec can verify without booting a driver at all. Moving those assertions down a level keeps the coverage while removing the most expensive part of the run.
- System and feature specs are a minority of examples but the majority of wall-clock, because each boots a real driver and waits on rendering.
- A single shard that inherits heavy system specs becomes the long pole and stalls the whole pipeline.
- Many system specs assert controller or JSON behavior that a request spec can cover without a browser; moving those down a level is usually the biggest single win.
parallel_tests pays off, then plateaus on small runners
Sharding the suite with parallel_tests is the most reliable way to cut Rails wall-clock, and it scales nearly linearly while there are idle cores to absorb the work. Going from serial to four workers on a representative suite takes the run from roughly twenty-two minutes to about eight, close to a threefold improvement, and the curve keeps bending downward as long as the runner has cores to spare.
The plateau arrives the moment the worker count exceeds the cores the runner actually has. A two-core hosted runner cannot run four parallel workers without time-slicing them, so the speedup flattens and additional parallelism config buys nothing. This is the trap teams fall into: they tune parallel_tests carefully, see the gains stall, and conclude that sharding has hit its limit, when in fact the runner has hit its limit.
The fix is to match core count to the parallelism the suite can exploit. A right-sized runner with eight cores keeps the curve descending where a two-core box would have flattened, which is why the floor on Rails suite time is governed by the runner layer as much as by the test configuration. Cores that are cheap to provision are what let parallel_tests keep cutting rather than plateau.
Bundler caching is table stakes and still often broken
A cold Bundler install does real work on every run: it resolves the dependency graph against the lockfile, downloads each gem, and compiles native extensions for the ones that ship C. For a mature Gemfile that lands around a minute of pure setup before a single test executes, and it is repeated on every push even though the dependency set rarely changes between commits.
Caching collapses this to seconds, and the mechanism is simple in principle: key the cache on the Gemfile.lock so that an unchanged lockfile restores the installed gems instead of reinstalling them. The trouble is that the principle is easy to break in practice. A cache key that drifts when it should not, or a per-runner cache silo that a fresh runner cannot see, quietly reintroduces the cold install while the configuration still looks correct.
This is where a fleet-wide managed cache pulls ahead of the hosted cache. Instead of each ephemeral runner maintaining its own island of cache that the next runner cannot reach, a shared cache restores the same warm gem set to every job. The difference between a twenty-seven-second hosted-cache install and a five-second managed-cache install is exactly the variance that per-runner silos introduce and a shared cache removes.
Gem resolution and install time per run. · Source: Latchkey analysis (modeled)
Flaky system specs force the most expensive re-runs in the suite
Rails has a well-known catalogue of flaky failures, and almost all of them cluster in the slowest tests. Capybara timeouts when an element renders a beat late, race conditions in JavaScript-heavy specs, database deadlocks under parallel load, and asset-pipeline hiccups are transient by nature: they pass cleanly on a retry because nothing was actually broken. The environment hiccuped, not the code.
The cruel arithmetic is that because system specs are the most expensive tests, a flake in one of them triggers the most expensive possible re-run. A green change comes back red, the developer context-switches to investigate, finds nothing wrong, retries the job, and pays for the entire slow suite a second time. The wasted minutes and the lost focus compound on a team that merges often.
Self-healing runners attack this directly by retrying a step that fails on a known-transient signal, on a fresh environment, before a human ever sees the red check. The mechanical majority of Rails flakes never reach the pull request, the developer never loses the afternoon, and the minutes spent become recovery minutes rather than wasted ones. Quarantining or rewriting the test is the wrong first response when the test was never the problem.
Asset builds and setup are the quiet tax on every run
Beyond specs and Bundler, every Rails CI run pays a setup tax that rarely shows up in anyone's mental model of the pipeline: precompiling assets, preparing and migrating the test database, booting the Rails environment, and running the linters. Individually these are small, but they recur on every job and across every shard, so on a heavily sharded suite the fixed setup cost gets paid many times over.
The recurring nature is the problem. A team that splits its suite across eight workers to go faster has also multiplied the per-shard setup work by eight, and if the database schema load or the asset build is not cached, each shard redoes it cold. The speedup from parallelism is real, but the setup overhead claws back part of it whenever that overhead is not shared across the workers.
The remedy is to treat setup the way teams treat dependencies: cache the compiled assets and the prepared schema where they are stable, and load them rather than rebuild them on each shard. This keeps the parallelism dividend intact instead of letting fixed setup cost erode it, and it is one of the reasons a cache that persists across the fleet matters more on Rails than the per-shard view suggests.
Managed runners change the Rails CI break-even
Rails CI is heavy Linux CPU and memory work: compiling native gems, booting browser drivers, and running parallel database-backed specs. That profile is exactly where an oversized self-hosted box sits idle between pushes and burns money, because CI load is spiky and a fleet sized for peak is mostly idle off-peak. The spreadsheet comparison of raw instance price to hosted per-minute rate omits that idle cost entirely.
Managed runners capture most of the per-minute compute savings of self-hosting while removing the idle and the operations, and Latchkey targets roughly 70% below GitHub-hosted rates. The relevant comparison for a Rails team is not instance price versus hosted price, it is total cost of ownership including the patching, scaling, and image maintenance that a runner fleet quietly demands from a team that often has no platform group to spare.
The second effect is the one that compounds with everything else in this report. Cheap extra cores are what let parallel_tests keep cutting suite time instead of plateauing, and a persistent fleet-wide cache is what keeps Bundler and asset setup warm across runners. The managed model lowers both the per-minute rate and the number of minutes a Rails build actually needs, which is why it changes the break-even rather than just trimming the bill.
- Rails CI is spiky Linux CPU work, the worst fit for a self-hosted fleet that sits idle off-peak.
- Managed runners price close to self-hosted compute while removing idle and ops; Latchkey targets roughly 70% below GitHub-hosted rates.
- Cheap cores keep parallel_tests scaling and a shared cache keeps Bundler warm, cutting both the rate and the minute count.
Published GitHub-hosted rates vs a managed alternative. · Source: GitHub Actions pricing + Latchkey rates
What the fastest Rails teams do differently
The Rails teams with the fastest pipelines do not have a secret gem. They share a short, unglamorous set of habits applied consistently: they cap suite duration explicitly and alert when it regresses, they keep the spec pyramid healthy so the slow browser tests stay a thin top layer, and they shard across enough cores to exploit the parallelism their suite allows.
They also instrument CI as a product surface rather than treating it as background plumbing. They track suite wall-clock, Bundler install time, flake rate, and cost per merge over time, and they treat a regression in any of those as a bug to be fixed rather than weather to be endured. Because they can see the curve, they intervene before a slow pipeline quietly trains the team to batch up large, risky changes instead of shipping small ones often.
- Bundler caching keyed to the lockfile, restored warm on every job rather than per-runner silo.
- parallel_tests scaled to the cores the runner actually has, not just the workers the config requests.
- A healthy spec pyramid that keeps slow system specs a thin top layer over fast request and model specs.
- Automated recovery for transient browser-driver flakes so mechanical failures never reach a developer.
- Tracked suite duration, install time, flake rate, and cost per merge, with regressions treated as bugs.
Recommendations
Rebalance the spec pyramid before buying bigger runners
Audit the slowest ten percent of specs and move every assertion that does not genuinely need a browser down to a request or model spec. This shrinks the most expensive layer of the suite without losing coverage, and it does more for wall-clock than a runner upgrade because it removes work rather than parallelizing it.
Cache Bundler against the lockfile and watch the hit rate
Key the Bundler cache on Gemfile.lock so an unchanged dependency set restores in seconds, then measure the cache hit rate the way you measure test coverage. A cache silo that a fresh runner cannot see silently reintroduces the cold install, so prefer a fleet-wide cache that restores the same warm gems to every job.
Match core count to the parallelism your suite can use
parallel_tests scales until it saturates the cores the runner has, then flattens. Size the runner to the worker count your suite actually exploits so the speedup curve keeps descending instead of plateauing on a two-core box, and cache per-shard setup so heavy sharding does not multiply the fixed cost.
Auto-heal transient flakes instead of quarantining specs
Capybara timeouts, JS race conditions, and database deadlocks are transient and pass on a clean retry. Retrying on a fresh environment removes the bulk of the Rails flaky tax without touching test code, and it keeps a red check caused by a late-rendering element from ever costing a developer an afternoon.
Treat CI metrics as a defended budget
Track suite duration, Bundler install time, flake rate, and cost per merge over time, and treat a regression in any of them as a bug. A slow pipeline that nobody watches quietly trains the team to batch up large changes, which is the opposite of how Rails is meant to be shipped.
Outlook
Expect the gap between Rails teams that treat CI as managed infrastructure and those still firefighting to widen through 2026 and into 2027. The levers compound: a team that caches Bundler, keeps a healthy spec pyramid, shards across real cores, and auto-heals transient flakes does not just spend less, it keeps the pipeline fast enough to encourage the small, frequent merges that Rails rewards. The teams that do none of these feel the opposite compounding as their suite grows.
The architectural direction is consistent with the rest of the CI landscape. Persistent fleet-wide caching, cheap elastic cores, and automatic recovery for mechanical failures are converging into a single expected baseline, and Rails benefits from it more than most stacks because so much of its CI cost is recomputed work and retried flakes rather than irreducible test execution.
For most Rails teams the practical takeaway is that a slow suite does not need a heroic rewrite to fix. It needs a duration budget, a few well-understood habits around caching and sharding, and a runner layer that keeps caches warm and absorbs transient failures automatically. The teams that internalize that will spend the next two years treating CI as an advantage while their peers keep paying a tax they could have removed.
Methodology
This report combines public Ruby ecosystem signals (RubyGems usage, Rails release cadence, and published GitHub-hosted runner pricing) with Latchkey runner analysis of Bundler, RSpec, and parallel_tests pipelines. Modeled figures reflect a representative mature Rails monolith with a system-spec suite, and are intended to show direction and magnitude rather than a precise population value. The CI adoption headline is from the Stack Overflow Developer Survey. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.
Sources
- Stack Overflow Developer Survey
- GitHub - Octoverse
- GitHub Actions - billing & pricing
- RubyGems - gem hosting