# Get in, human: cut Rails boot time with require-profiler and this guide

> Rails boot time is a DX metric in the AI age: meet require-profiler, learn to actually read sampling profilers, and see the pit stop that cut a 200-component monolith's boot by 40%.

- Date: 2026-08-19T00:00:00.000Z
- Authors: Artur Petrov, Vladimir Dementyev, Travis Turner
- Categories: Rails, DX, Performance
- URL: https://evilmartians.com/chronicles/get-in-human-cut-rails-boot-time-with-require-profiler-and-this-guide

---

Every Rails app race starts the same: load code, read config, then the green flag. But the bigger the app, the more startup drags on deploys, CI, and developer patience. This is doubly so as AI agents boot your app far more often than humans do. Thus, meet [require-profiler](https://github.com/palkan/require-profiler), a new one-stop profiler for Ruby's code loading process! Grab it first when optimizing boot, leaving sampling profilers for the deep dives. We'll show the telemetry from real projects: one cut the dev boot of [Factorial](https://evilmartians.com/clients/factorial)'s 200-component monolith by 40%. And read till' the final lap: profiling an [AnyCable](https://anycable.io)-powered app's boot uncovered a surprise in Ruby itself!

Now the usual objection: nobody has time to tune boot. It sounds like a week of work with no guaranteed payoff (though here the prize compounds with every boot, every day). But with the right tools, your first pass takes an afternoon, and most of the fixes in apps are one-liners. (Besides, the framework's creator is literally a [Le Mans class-winning driver](https://dhh.dk). Speed is practically canon.)

We've poked at boot before on [Railsmas day 2](https://evilmartians.com/chronicles/railsmas-on-mars-12-days-of-mandatory-developer-joy-and-challenge#day-2-faster-rails-boot-time), and the other 11 days hold more goodies.

## Lap 0: Bootsnap

Before entering the profiling race, make sure [Bootsnap](https://github.com/rails/bootsnap) is on and working. It's the easiest possible win at the start. Legacy apps occasionally run with it missing entirely (we've seen it multiple times), and that's where the huge wins hide. On Factorial's monolith, the same boot runs 2.6 times slower without it. It ships enabled in new Rails apps, so the checklist is really two items: keep it on and  *updated*, and precompile it properly for production.

We put *updated* in that checklist on purpose. The ecosystem's common curse is that gems ship speedups, and nobody collects them. We'll meet this pattern again several times in this post. In Bootsnap's case, recent versions roughly halve the load-path scan by cutting `stat` syscalls (an optimization also headed to Ruby 4.1 as a new `Dir.scan`). Bump the gem to at least `1.20.0` and win your first seconds.

The full story of that optimization is in [Jean Boussier's write-up](https://byroot.github.io/ruby/performance/2026/04/18/faster-paths.html), along with the interesting `File.join` history and speedups uncovered by the same profiling effort.

Now for production, don't forget [precompilation](https://github.com/rails/bootsnap?tab=readme-ov-file#precompilation). In development the cache warms up *automagically*. But a production image gets no warm-up laps, so "pre-warm" the cache during the Docker build:

```bash
bundle exec bootsnap precompile --gemfile app/ lib/
```

Include all the code directories (i.e., `config`, `engines`, `packs`, and other custom top-level directories). To verify nothing is missed, enable the instrumentation and boot:

```ruby
# config/boot.rb
Bootsnap.instrumentation = ->(event, path) { puts "#{event} #{path}" }
```

```bash
rm -rf tmp/cache/bootsnap/*
bundle exec bootsnap precompile --gemfile app/ lib/
bundle exec rails runner 'puts :ok' | grep 'miss'
```

And watch your Docker setup. The cache lives in `tmp/`, and that location invites accidents: a multi-stage `COPY` can skip it, a cleanup step can wipe it, and a volume can shadow it at runtime.

## Lap 1: require-profiler

For years, newbies started their boot profiling races with [Bumbler](https://github.com/nevir/Bumbler). It's a flat list of per-gem require times, plus a separate mode for initializers. But it never tells the whole story, though. Transitive requires get blurred, whatever a gem *does* while loading hides inside one opaque number, and the Ruby code that runs past the requires remains totally invisible. 

On Factorial's monolith (before the pit stop), it flagged just six gems, barely over a second combined, `rails` itself and `anycable-rails` among them (remember that last one for the final lap). Not much to act on when the stopwatch showed ~22 seconds with Bootsnap enabled.

So, let's welcome a new contender to the race: [require-profiler](https://github.com/palkan/require-profiler), an easy-to-use one-stop shop for nearly every boot issue. It profiles Ruby's code loading itself: every `require`, `require_relative`, and `load`, as a tree, with timings. And a plugin system covers the costs requires alone can't see. The gem also has a fascinating origin story.

It was built on [require-hooks](https://github.com/ruby-next/require-hooks), a universal way to intercept Ruby's code loading process. Despite Ruby's famous flexibility, overriding the loading is officially unsupported. Yet everyone seems to love patching it anyway (RubyGems, Zeitwerk, Bootsnap), and resolving the resulting conflicts is _hacky as hell_. So Vladimir Dementyev [made the case for a proper loader API in Ruby itself at RubyKaigi 2026](https://evilmartians.com/events/require-hooks-rubykaigi), with require-profiler premiering as the talk's bonus track.

Another require-hooks-born trick is [Freezolite](https://github.com/ruby-next/freezolite): `frozen_string_literal` for all your files, no magic comments. The idea recently [landed in Bootsnap itself](https://github.com/rails/bootsnap/pull/535) and powers [the Rails move to frozen literals by default](https://github.com/rails/rails/pull/57252).

Add it to the Gemfile and point it at your app:

```sh
bundle exec ruby -r./config/boot -require-prof config/environment.rb
```

Puzzled how we added a new option straight to Ruby? The gem ships a file named `equire-prof.rb`, so the flag parses as `-r equire-prof` (same trick as Ruby's own [`-run`](https://github.com/ruby/ruby/blob/master/lib/un.rb)).

Loading `config/boot.rb` first sets up Bundler and Bootsnap (remember the conflicting patches? Bootsnap must hook in first). Then everything starting from `config/environment.rb` gets profiled. (If the app still carries [Spring](https://github.com/rails/spring), disable it while profiling with `DISABLE_SPRING=1`, otherwise you're timing a fork, not a boot.)

The output is an indented tree: one line per loaded file with its time in milliseconds, self plus children, and a few synthetic lines mixed in. The rough shape is this:

```
entry_file.rb — self + children time, in ms
  required_file.rb — ...
  parsed_file.yml — ...
  prefix:synthetic_line — ... (an HTTP call `http:` or a Rails phase `rails:`)
```

And here's a slice of Factorial's pre-pit-stop real profile (shortened):

```
config/application.rb — 6198.333ms
  components/integrations_google_users/lib/integrations_google_users.rb — 177.365ms
    gems/google-apis-admin_directory_v1-0.42.0/lib/google/apis/admin_directory_v1.rb — 125.054ms
rails:initializer:load_config_initializers:gems/railties-7.2.2.2/lib/rails/engine.rb:640 — 791.744ms
  config/initializers/countries.rb — 1369.053ms
    gems/faker-2.19.0/lib/locales/ja/address.yml — 357.229ms
    gems/faker-2.19.0/lib/locales/es.yml — 6.947ms
  config/initializers/http_showcase.rb — 39.739ms
    http:GET:https://www.ruby-lang.org/ — 11.574ms
rails:initializer:run_prepare_callbacks:gems/railties-7.2.2.2/lib/rails/application/finisher.rb:73 — 4872.904ms
  rails:to_prepare:gems/karafka-2.5.2/lib/karafka/railtie.rb:87 — 830.3ms
    karafka.rb — 830.111ms
  rails:load_hook:after_initialize:karafka.rb:20 — 705.965ms
```

The slice above shows all four kinds of boot cost. Plain requires sit at the top: here, the Google gem load. The `rails:` lines track the whole Rails initialization pipeline: initializers, then reload callbacks, finally load hooks. The YAML lines are files parsed during boot, better caught on a cold rerun since Bootsnap can make them too fast to notice. And the `http:` lines are network calls made along the way; ours is planted for the showcase, though real ones sneak into apps surprisingly often.

Still overwhelmed? The real report goes on like this for thousands of lines, drowning real issues in harmless ones. You can filter out the noise: `REQUIRE_PROFILE_THRESHOLD` drops everything faster than N milliseconds, and `REQUIRE_PROFILE_FOCUS` zooms into a pattern, ancestors included, so chains stay readable:

```sh
# Only files that took 100ms or more
REQUIRE_PROFILE_THRESHOLD=100 bundle exec ruby -r./config/boot -require-prof config/environment.rb

# Zoom into a single gem, full require chains included
REQUIRE_PROFILE_FOCUS="aws" bundle exec ruby -r./config/boot -require-prof config/environment.rb
```
Radio channel clear: "_Okay, filters on and telemetry reads clean. One kind of line explained at a time, please._"

### The requires: the heaviest gems

```
  components/integrations_google_users/lib/integrations_google_users.rb — 177.365ms
    gems/googleauth-1.5.2/lib/googleauth.rb — 51.428ms
    gems/google-apis-admin_directory_v1-0.42.0/lib/google/apis/admin_directory_v1.rb — 125.054ms
  gems/hexapdf-1.7.0/lib/hexapdf.rb — 62.873ms
  gems/caxlsx-3.4.1/lib/caxlsx.rb — 51.834ms
  gems/savon-2.15.0/lib/savon.rb — 92.329ms
  gems/elasticsearch-8.13.0/lib/elasticsearch.rb — 104.781ms
    gems/elasticsearch-api-8.13.0/lib/elasticsearch/api.rb — 98.503ms
  gems/karafka-web-0.11.4/lib/karafka/web.rb — 243.248ms
    gems/karafka-2.5.2/lib/karafka.rb — 142.431ms
  lib/frontend_navigation.rb — 75.717ms
```

You'll meet the same offenders in app after app. The heaviest are usually generated API gems (a favorite sin of cloud providers), heavy-processing libraries (PDF, XLSX, etc.), and big transport clients like `savon` (SOAP), `elasticsearch`, and `karafka` (Kafka). Luckily, the majority of them are rarely needed at boot, and some gems can be partially loaded. 

For internal dev tooling (admin UIs, `bullet`, console enhancements, stray test tooling, and the like), gem groups are a solution when the default environment can skip a dependency. The habit to build: keep an eye on heavy dependencies, and when only one corner of the app needs them, load them there instead of on every boot.

Gems aren't the only suspects, though. Any plain app file can lose time the same way, because everything at class or module level runs *during* the require. The lineup's last line is a homegrown example: `lib/frontend_navigation.rb`, a generated map of every frontend route that lets backend code build typed links into the UI. Factorial pays for it in boot time by defining thousands of classes at load time.

So, big DSLs are a high-risk tax to pay. Like muscle cars. Impressive, but _thirsty_. GraphQL, Grape, and Active Admin are the notorious ones that we've met each on different projects. These parts aren't always easy to delay or skip conditionally, but manual optimizations and caches are always available to racers.

### YAML lines: what the tire warmers can hide

```
# A warm run
  config/initializers/countries.rb — 447.26ms
    gems/faker-2.19.0/lib/locales/ja/address.yml — 182.118ms
    gems/faker-2.19.0/lib/locales/es.yml — 2.831ms
# A cold rerun (`DISABLE_BOOTSNAP=1`)
  config/initializers/countries.rb — 1369.053ms
    gems/faker-2.19.0/lib/locales/ja/address.yml — 357.229ms
    gems/faker-2.19.0/lib/locales/es.yml — 6.947ms
```

The require tree comes from hooking Ruby's code loading, but not every boot cost is a require. Catching the rest is the gem plugin system's job. YAML is the built-in plugin: every file parsed during boot gets its own line in the tree, no setup needed. Bootsnap can mask these lines, though: it caches the parsed result and serves it back on warm boots. So profile twice, with the cache and without (`DISABLE_BOOTSNAP=1`).

The block above is the same `countries.rb` line, warm and cold side by side. Warm, Bootsnap serves the already-parsed YAML from its cache, and the heaviest files still surface, since serving the cache isn't free either. Cold, every file gets parsed for real, a second slower in total, and the true source arrives: `faker`'s Japanese address book is not what anyone ordered (unless you're a big fan of _The Fast and the Furious: Tokyo Drift_). 

Here's what actually happened: `faker` appends every locale it ships to `I18n.load_path`, and we asked the `countries` gem to register a custom country before I18n was configured. That made I18n parse the entire load path: hundreds of files, all thrown away by the reloader before the first request.

I18n is the repeat offender behind most of this. Usually it hides in less exotic places though: model validations love to eagerly resolve translation messages at class-load time, preloading the entire locale set. So watch anything that touches I18n before the app is fully configured: class-level `I18n.t` calls in constants or validation messages (use the lazy block version), and locale-hungry gem setup in early initializers.

### HTTP lines: off-track excursions

```
# A synthetic showcase
  config/initializers/http_showcase.rb — 39.739ms
    http:GET:https://www.ruby-lang.org/ — 11.574ms
# Another project
  config/initializers/fragment.rb — 2064.081ms
    http:POST:https://api.fragment.dev/graphql — 512.632ms
```

YAML isn't the only plugin in the box: add the [sniffer](https://github.com/aderyabin/sniffer) gem to the Gemfile, and HTTP requests show up as `http:`-prefixed lines. The first block is our planted evidence: `http_showcase.rb` exists only to show the line shape, since Factorial's boot makes no network calls. In other projects, though, real ones get added and forgotten more often than expected. 

Here's how to hunt for them:

```sh
REQUIRE_PROFILE_FOCUS="http:" bundle exec ruby -r./config/boot -require-prof config/environment.rb
```

Sniffer races in another series too: [Isolator](https://github.com/palkan/isolator) is built on it to catch non-atomic interactions within DB transactions, such as network requests.

An HTTP call during boot sounds exotic until you go looking: at [Whop](https://evilmartians.com/clients/whop), it was added by a third-party dependency (the second block in the snippet). Each one adds latency to every single boot and a fun new failure mode when the network hiccups mid-deploy.

### Rails lines: the factory telemetry

```
rails:initializer:setup_main_autoloader:gems/railties-7.2.2.2/lib/rails/application/finisher.rb:18 — 1829.278ms
rails:initializer:run_prepare_callbacks:gems/railties-7.2.2.2/lib/rails/application/finisher.rb:73 — 4872.904ms
  rails:to_prepare:gems/karafka-2.5.2/lib/karafka/railtie.rb:87 — 830.3ms
    karafka.rb — 830.111ms
rails:initializer:finisher_hook:gems/railties-7.2.2.2/lib/rails/application/finisher.rb:93 — 2086.614ms
  rails:load_hook:after_initialize:karafka.rb:20 — 705.965ms
```

The newest trick grew straight out of profiling monoliths like Factorial: the profiler now captures the Rails initialization pipeline as first-class `rails:` lines. `initializer:` covers railtie initializers (one-time setup steps that gems and the app register for boot), `to_prepare:` the reload callbacks (code that reruns on every code reload in development), `load_hook:` the lazy load hooks (deferred setup that fires when a framework part first loads, like `ActiveSupport.on_load(:active_record)`). 

This closes the last thing the require tracking left open, because on a big app the requires are only half the story. At Factorial, the `initialize!` phase ate roughly two-thirds of the whole boot, and the `run_prepare_callbacks` line above claimed a fifth on its own.

The first culprit we found at Factorial is relevant to lots of Rails apps: `Devise`. By default, it [forces all application routes to be reloaded](https://github.com/heartcombo/devise/commit/eba2527b8e730907c51da8eceaa9b996b865523a), effectively loading them twice. The cure is one config line in `config/initializers/devise.rb` (try it on your next race too). On another routes-heavy app we audited earlier, this line alone once cut the boot from ~31 to ~23 seconds:

```ruby
# When false, Devise will not attempt to reload routes on eager load.
# This can reduce the time taken to boot the app but if your application
# requires the Devise mappings to be loaded during boot time the application
# won't boot properly.
config.reload_routes = false
```

Another culprit was `to_prepare` hooks carrying crunch work that runs on every boot *and every dev reload*. Factorial leans heavily on centralized registry code catalogs, and two of these hooks constantized almost every app class into lists on each pass, feeding some skip logic down the line. Only rare code flows ever read the result. Both hooks became lazy: they resolve on first use and stay memoized until the relevant code actually changes. 

Be careful with such changes, though: lazy work on a hot path can grow the tail latency. So always mind where a boot win lands: some fixes speed up development only (like the disabled global requires and the gem-group moves), `to_prepare` ones pay off on every reload, and some reach production boots too, for good or bad (like the I18n fixes).

Initializers also love opening connections (eager database checks, Redis connections, etc.), and those stay under speed radar even to the HTTP plugin: `sniffer` can intercept HTTP clients, not raw TCP. If an initializer line is heavy with nothing beneath it, suspect the network first.

Did you hear the announcement? Rails 8 now includes [global lazy routes](https://github.com/rails/rails/pull/52353) by default—large route sets finally stopped costing time on the grid.

Another `rails:` find at Factorial had consequences far beyond its own line: Karafka. Its railtie dragged the whole consumer graph into every process just to keep the topic topology on hand for producers (rarely needed in other process types). Along the way, it cost the app its [lazy routes](https://github.com/amatsuda/routes_lazy_routes). Now Karafka loads only in its consumer processes, and the routes are lazy again for everyone else.

Back in the profile slice, Zeitwerk shows up too: the `setup_main_autoloader` line, weighing almost two seconds. Part of the cure was upstream: Zeitwerk 2.7.4 and up ship a [validation optimization](https://github.com/fxn/zeitwerk/pull/322) that pays off exactly at this size (and it's [not the first time](https://github.com/fxn/zeitwerk/pull/308) a project this big has pushed the autoloader to its limits). The updating curse strikes again: bump the gem and collect the win.

Years of postponed upgrades? Measure the gap in libyears, the laps you're down on the leader:
*Read also Evil Martians' article*: https://evilmartians.com/chronicles/railsmas-on-mars-12-days-of-mandatory-developer-joy-and-challenge

And one of the deepest cuts hides in the same autoloader corner: [`config.add_autoload_paths_to_load_path = false`](https://guides.rubyonrails.org/configuring.html#config-add-autoload-paths-to-load-path), the default since Rails 7.1 (Zeitwerk never consults `$LOAD_PATH` anyway). Same story as before: options like this sit uncollected for years.

The final find again traces back to the monolith's size: the structure sends one more bill. Each of the example app's 200+ components is a Rails engine under the hood (with Packwerk guarding the boundaries), and sorting their initializers hit an O(n²) path in railties' `tsort` usage, costing seconds on every boot. [Rails 8.1 ships the fix](https://github.com/rails/rails/pull/53615). Until the upgrade hits, a guarded local patch holds the position.

### The whole boot on one screen

Whoa, pump the brakes there! The project we're speeding through carries over 200 components? Yes, Factorial, like lots of big codebases, takes the component/pack architecture to heart. This has its evident drawbacks (several slowdowns above trace back to the deep filesystem hierarchy) and its perks. 

And one of the perks deserves a special showcase, but first we need to learn another, a more graphical way to read the report to understand it. The same profile exports as JSON and opens in [speedscope](https://www.speedscope.app/)'s neat UI:

```sh
REQUIRE_PROFILE_PATH=tmp/require-profile.json bundle exec ruby -r./config/boot -require-prof config/environment.rb
```

Three views are in the spotlight here. Time Order replays the boot left to right: the requires first, then the `rails:` phases, in the order they really ran. Unlike a sampled flamegraph, every millisecond on this axis is an exact load duration:

*Image: The whole boot on one screen: requires on the left, `rails:` phases on the right—and every millisecond on the axis actually happened*
Left Heavy regroups the same data by weight: identical subtrees merge, and the widest offenders float to the left, regardless of how scattered their loads were in time:

*Image: `run_prepare_callbacks` is the heaviest phase of the boot*
Sandwich ranks every frame by self time; the files that are slow on their own, not through their children (it's also the simplest view to find the next cue):

*Image: The top 10 slowdown contenders*
This is where the promised perk of the component structure finally shines. The profiler emits every path segment as its own frame, so costs stack by directory: `components/#{name}/` blocks stand side by side on the screen, and every slow millisecond arrives with a code owner attached.

Yet there is still a blind spot worth noting. It can mark some line in the report as heavy, but never explain *why*. And some costs never get a line at all: for example, a bloated `$LOAD_PATH` taxes every require a little, everywhere at once. The answers hide below the require machinery, in Ruby internals, native code, and syscalls. Reading that layer takes a sampling profiler—the co-driver with the pace notes.

## Lap 2: reading a sampling profiler without spinning out

So if you've ever needed to look inside the engine bay, down at Ruby internals—and let's be honest, this is where many developers bail out and defer to the "mechanics"—here's a short (extreme) driving course for you.

The sampling idea itself is simple: the profiler interrupts the program hundreds of times per second and records the current call stack. Time is attributed statistically; a method "taking N%" was on the stack in N% of samples. Nothing is traced exactly, which is why you should trust percentages over milliseconds on short boot runs. require-profiler integrates natively with [Stackprof](https://github.com/tmm1/stackprof):

[Vernier](https://vernier.prof) works here too, with a richer UI: threads, allocations, and GC are also visible, though these parts are less relevant to boot.

```sh
REQUIRE_PROFILE_STACKPROF=warden/cognito/test_helpers.rb bundle exec ruby -r./config/boot -require-prof config/environment.rb
```

That writes a speedscope-ready JSON next to the raw dump.

Reading the result takes one skill: telling *total time* (a method plus everything it calls) from *self time* (the work done in the method's own body). A frame with huge total and near-zero self is a manager, not a worker. Drill into its children until self time shows up.

The views read the same on sampled dumps: Left Heavy turns thousands of samples into wide, readable bars, and Sandwich surfaces the native frames immediately, C code and syscalls included.

The first sampler find paid off quickly at Factorial: extra filesystem traversal, caused by deep directory nesting (components inside components), dragged the boot by over half a second. That's pure `Dir.glob` work, syscalls with no tree line of their own. Fixed-depth globs instead of `components/**` walks cured it. An extreme case at this size, granted, but checking for excessive work wins again, this time via sampling.

And the second find was a brand-new catch. The require tree blamed a file in a local Warden fork—and that's all we could say. The sampler showed the rest: the time was sinking into OpenSSL key generation, native code invisible to any require-level view:

{% image "stackprof-sandwich.png", caption: "OpenSSL key generation: 88% of the file's load, all self time", alt: "speedscope Sandwich view over the Stackprof dump" %}

It costs up to half a second when the machine runs low on entropy. The same C boundary hides the classic native extension init (`grpc`, `sassc`, etc.): one `require` kicks off the whole library setup, and Ruby sees none of it. C boundaries are where samplers pay for themselves.

So here's the final plan. Run require-profiler first: plain requires, initializers, YAML, and HTTP, all in one readable pass. Drop to the sampler only for the lines the tree can't explain.

> With that, Factorial's pit stop is over: the boot went from roughly 23 to 13 seconds, 40% off, with no obvious single hotspot left behind. Production got about 20% off too (several fixes were development-only), and every deploy now clocks faster.

Past that point you're not removing bolts, you're redesigning the car. One such possible redesign is *scoped boot*: each process type loads only its own components, so no web code in Sidekiq (another benefit of the component architecture). That's a story for another legendary ride though.

## One last ride: AnyCable's own boot race

Remember `anycable-rails` on Bumbler's shortlist at the very start? Framework gems end up on every leaderboard sooner or later, and nothing keeps a toolmaker honest like pointing the tool at their own gem. And yes, the surprise promised at the start finally shows up here.

So we profiled an [AnyCable](https://anycable.io)-powered app's boot again, this time a bare demo. `anycable-rails` topped the leaderboard (what a win!) at 112ms, ahead of `rails` itself at 78ms, before a single cable was even connected.

require-profiler broke it down without drama. The first 10ms was dead weight: [Anyway Config](https://github.com/palkan/anyway_config)'s Doppler secrets loader required `net/http` at load time, pulling in the whole HTTP stack whether or not you use Doppler. Another 10ms came from testing-framework patching that lived in the production path. After our patches, it only kicks in when the Action Cable test classes are actually loaded. The gRPC stack added about 20ms more with nothing to trim inside.

Need to trim the gRPC part anyway? Try [`anycable-rails-core`](https://github.com/anycable/anycable-rails): the same gem without Google's hefty bundled gRPC server. Bring a lighter Ruby integration instead, or skip the gRPC integration entirely.

The remaining chunk in the report was refinements, the biggest surprise of the race. Rewriting the gem's refinement-based patching as plain class reopening saved 10ms more, which made no sense at first: refinements aren't supposed to cost anything at *definition* time. A quick test said otherwise: activating them late in the boot, with ~30,000 classes around, is wildly more expensive than early, at ~4,000. [An isolated benchmark](https://gist.github.com/ardecvz/fdd33501625199018fb698cfea8d8537) confirmed it universally: on Ruby 3.3, `using` overhead grows with the class count, adding 50% or more at a real app's scale. The cause is [Ruby issue #21201](https://bugs.ruby-lang.org/issues/21201): every refinement activation invalidates method caches through a full object-space scan. Ruby 4.0 fixed it with a dedicated refinement-cache table (unfortunately, no backports to older versions), and the same benchmark there shows the overhead more than ten times smaller.

Verify from your own pit box: [the benchmark](https://gist.github.com/ardecvz/fdd33501625199018fb698cfea8d8537) is one file—generate N classes, require `anyway_config`, compare with `NOUSING=1` across Ruby versions.

What to do if you're an AnyCable user? Upgrade Ruby regularly, to 4.0 and up in this case. Free optimizations help whether you know about them or not, and regular upgrades make each one easier. Stuck on an older Ruby? Then require refinement-heavy gems (`anyway_config`, `anycable-rails`) early in the Gemfile, right after `rails/all` (luckily, such gems are rare in the ecosystem).

After the fixes, `anycable-rails` ducked back behind Rails in the standings ...the only race where the maintainer celebrates losing a position.

## Handing the wheel to your AI robot family

Now we'll go from tutorial to future infrastructure. require-profiler ships its own instructions for AI agents: a [`SKILL.md`](https://github.com/palkan/require-profiler/blob/master/skills/rails-boot-profiling/SKILL.md) distributed via [Rails Hyperdrive](https://github.com/Bakaface/rails-hyperdrive), the gem that lets any Ruby library deliver skills and guidelines straight into your agent's context:

```sh
bundle add rails-hyperdrive require-profiler --group development
bin/rails hyperdrive:init

      create  .mcp.json
      append  .gitignore
      append  Gemfile
      insert  config/routes.rb
      create  .claude/skills/rails-boot-profiling/SKILL.md
      create  .hyperdrive/lock.yml

        done  hyperdrive initialized
```

The skill packs everything handy from this ride: the baseline measurement run, threshold-then-focus narrowing, the YAML and HTTP checks, the Stackprof handoff, and a checklist of the common offenders above. So the next time someone types "my app started booting slowly" the agent loads the skill as relevant, runs the profiler, and drives you to a quicker land in full self-driving mode (beware: please keep your hands on the steering wheel, it may do the wrong thing at the worst time, and the driver is responsible for all collisions with production).

*Image: Type *

We're collecting more skills like this, across all stacks, in [our skills section](https://evilmartians.com/skills). Each one is a piece of engineering practice you can grab and install manually.

*Download Evil Martians' agent skill*: https://github.com/palkan/require-profiler

The pit board, in five lines:

- **Lap 0:** keep Bootsnap enabled, updated, and precompiled for production. Remember it can mask YAML costs while you profile by serving pre-parsed caches, so add a cold rerun.
- **Lap 1:** run require-profiler first. The require tree with its plugins covers nearly every boot issue, and most fixes are one-liners. Consult the common offenders in the article.
- **Lap 2:** when the tree shows a file but not the reason, scope in with `REQUIRE_PROFILE_STACKPROF`. Native code is where samplers pay off.
- **Final lap:** profile third-party gems and frameworks too, especially the ones you maintain.
- **Victory lap:** install the skill via Rails Hyperdrive and let your agents help run the whole playbook.

Every Rails app race still starts the same, but now AI agents run it hundreds of times a day, and every boot is on the clock. Reach for [require-profiler](https://github.com/palkan/require-profiler) first (almost one-stop, easy to use), and save the sampling profiler for the rare corners the tree can't explain. Teams that internalize profiling ship more than the ones still shopping for a bigger engine.

And somewhere at Le Mans, perhaps someone salutes.

---

**Put a Martian pit crew on your Rails app!** Evil Martians profile, tune, and upstream fixes for Rails apps of every size: from MVP to 200-component monoliths. Box, box, box! [Contact Evil Martians](https://evilmartians.com/contact-us)
