Autoload and load both bring Ruby classes into memory, but they trigger at different moments and carry different risks. Understanding when constants resolve changes how you structure gems, boot apps, and debug production issues.
Autoload promises faster boot by deferring work until the constant is first referenced. Load executes immediately, ensuring everything is present before your first line of user code runs.
Core Mechanism: How Each Hook Fires
Ruby installs an anonymous class-level Proc for every autoloaded constant. The moment the interpreter sees an otherwise undefined constant, it fires that Proc, requires the file, and replaces the constant’s entry in the lookup table.
Load bypasses that indirection. Kernel#load reads the file, evaluates it top to level, and leaves the constant visible right away. No hidden Proc, no lazy trigger, no second chance.
Constant Lookup Path Differences
When Ruby resolves an autoloaded constant it walks the nesting stack once, finds the autoload marker, and then restarts the same lookup after the file is required. This restart can pick up a different constant if the file defines something unexpected.
With load the constant is already materialized, so further lookups hit the same object identity on the first pass. That predictability matters when you reopen classes later.
Memory Footprint at Boot
A medium-sized Rails app can declare 1,200 models, helpers, and serializers yet touch only 80 of them during a typical web request. Autoload keeps the other 1,120 files off the heap until a code path actually needs them.
Load front-loads every class, so the master process on forking servers like Unicorn or Puma consumes an extra 60-90 MB before it even accepts requests. That multiplied by 8-16 workers pushes smaller dynos into swap.
Memory saved by autoload becomes more pronounced in background job workers that rarely enqueue every model. Sidekiq processes that lazily load 30 % of code use 40 % less RAM on Heroku 512 MB containers.
Thread Safety and Race Conditions
Ruby 3.1 made autoload thread-safe at the C level, but the guarantee only covers the require itself. If the loaded file mutates global state—class variables, singleton methods, or memoized module variables—two threads can still interleave those assignments.
Load removes that window because the file runs synchronously while the application boot lock is still held. Once boot finishes, no thread will encounter a half-initialized constant.
A practical safeguard is to wrap autoloaded files that modify globals in a Mutex.synchronize block. That keeps the deferred load safe without forcing eager load on the whole codebase.
Reloading in Development Mode
Rails’ classic autoloader relied on const_missing to unload and re-require files on every request. Zeitwerk replaces that with a hash-backed index, but it still depends on autoload so it can remove the constant and reinstall the marker.
Load defeats reloading because the constant is already defined; removing it triggers warnings and leaves stale references in subclasses. That is why Rails switched to autoload by default even in development.
If you manually load a file in config/initializers, you must also add Rails.application.reloader.to_prepare do … end to re-load it on each code change, or you will see bizarre “method undefined” errors after the second request.
Production Performance Impact
Deferring 1,000 files sounds efficient, yet the first real request pays the price. New Relic data shows median response time on an autoload-heavy endpoint can spike to 900 ms for the 95th percentile while the same endpoint on eager load stays at 220 ms.
The spike disappears once every relevant constant has been referenced, so many teams run a single warmup request after deploy. Tools like bootscale or zeitwerk’s cache further cut constant lookup time, narrowing the gap to 5-10 %.
For APIs that spin up short-lived containers, eager load plus frozen string literals beats autoload plus warmup because container start time dominates. Autoload wins for long-lived pods that field diverse traffic patterns.
Error Location and Stack Traces
When autoload fails, the backtrace points to the line that first mentioned the constant, not to the faulty file. Developers often grep for the class name and land in unrelated code.
Load puts the exception inside the required file, so the filename and line number in the trace match the actual syntax error. That saves minutes during deploys when a missing comma would otherwise look like a routing bug.
A mitigation is to set RubyVM::InstructionSequence.compile_option = { trace_instruction: true } in development. It annotates autoload frames so the debugger can show both the call site and the require site.
Use in Gem Design
Gems that target CLI tools should avoid autoload because cold-start latency matters more than memory. RuboCop’s early versions used autoload and shaved 180 ms off boot by moving to eager load.
Library gems meant to be composed into larger apps benefit from autoload. RSpec lazily loads formatters, so apps that only use the progress formatter never pay for the HTML or JSON ones.
Document the choice in the README. Many developers preload entire gem folders in test suites; if your gem autoloads, provide an optional eager-load entry point so consumers can opt into stability.
Interaction with Module Prepending
Autoload runs after Ruby has resolved the constant, so prepending a module to that constant inside the same file works transparently. Load makes the constant available earlier, which can change prepend order if another initializer references it.
A concrete issue arises with monetize gem and Rails 7: monetize prepends to Numeric, but if an initializer loads a decorator that also prepends to Numeric, the order differs between eager and lazy modes. Tests pass locally with autoload yet fail in CI with eager load.
Fix it by moving the prepend into a Railtie initializer with explicit before: :load_config_initializers. That guarantees consistent placement regardless of load strategy.
Testing Strategies
Autoloaded code can hide uninitialized constant errors until a specific test runs. Running tests in random order with –order rand surfaces those bugs faster.
Use Zeitwerk’s loader.eager_load in spec_helper to force every file into memory before the first example. The suite slows by 8 % but eliminates order-dependent failures.
Combine both approaches: let CI run eager load for coverage, and let developers run the default autoloaded suite for speed. A single line in .gitlab-ci.yml sets an ENV variable that toggles the loader.
Security Considerations
Autoload can be hijacked if an attacker controls a file path. Ruby 3.2 restricted autoload to only accept absolute paths when $SAFE > 0, but most apps run with $SAFE = 0.
Never construct autoload paths from user input. A registration form that writes to app/models/user_#{params[:type]}.rb and then autoloads User::Params[:type] opens a code injection hole.
Load is equally dangerous if you eval user data, yet it is easier to audit because the call happens at a single point in time. Search your codebase for load, require_relative, and autoload to create an allow-list of trusted paths.
Practical Decision Matrix
Choose autoload when memory is constrained, boot time is noticeable, and the app can tolerate a one-time warmup hit. Mobile back-ends on Heroku free tier fit this profile.
Choose load when you need deterministic initialization, share state across threads at boot, or run short-lived workers where container start dominates. Data pipeline Lambdas often eager load for this reason.
Mix both: keep framework code autoloaded for fast restart, but load business-critical paths that register observers or append to global arrays. A one-line initializer can require the handful of files that must run upfront.