rails/rails#58861 ships no new API. It’s a 29-commit rewrite of one guide — “Autoloading and Reloading Constants” — reviewed directly by Zeitwerk’s author, Xavier Noria. That’s worth reading anyway, because the parts it rewrites are the parts most Rails developers have a working understanding of rather than an actual understanding of: why require disappears in Rails controllers, why reload! doesn’t do what the name implies, and why some classes are explicitly forbidden from being reloadable. Here’s what’s actually in it, with the examples pulled straight from the diff.
Why Rails controllers never call require
The guide opens with the thing that makes autoloading necessary in the first place: in Ruby, a class or module’s name is just a constant, and nothing connects a file to the constant it defines. user.rb and User have no inherent relationship — Ruby only learns about User once something actually loads that file.
# -----------------------# Do not do this in Railsrequire "application_controller"require "post"# -----------------------
class PostsController < ApplicationController def index @posts = Post.all endendRails controllers never have those require lines, and the rewritten intro is explicit about why: a Zeitwerk-based loader has already registered every constant in your autoload paths with Ruby’s own Module#autoload hook before your code ever runs.
Object.autoload(:User, "#{Rails.root}/app/models/user.rb")The first time your application references User and finds no such constant defined, Ruby itself consults that registered entry and requires the file — Rails isn’t intercepting constant lookups, it’s handing Ruby’s own built-in autoload mechanism a complete map of file paths before boot finishes.
main vs once — one line of distinction, not a wall of config
Previous revisions of the guide explained autoload_paths and autoload_once_paths as two unrelated settings you configure independently. The rewrite names the actual distinction up front: there are two autoloaders, main and once, and the only difference between them is whether what they load gets reloaded later. Everything in app is managed by main by default; once exists for the narrower case of code that needs to autoload without ever being swapped out from under a reference someone else is holding.
That reframing matters because it turns config.autoload_lib_once from a separately-memorized setting into an obvious consequence of the same idea:
module MyApp class Application < Rails::Application config.autoload_lib_once(ignore: %w(assets tasks)) endend# lib/money_serializer.rb defines MoneySerializer — no `require` needed,# and because it's managed by `once`, this is safe in an initializer:Rails.application.config.active_job.custom_serializers << MoneySerializerIf MoneySerializer were reloadable instead, that same initializer line would raise a NameError — initializers run once, at boot, and Rails disallows referencing main-managed constants there specifically because editing them later wouldn’t update whatever already captured the stale reference.
Reloading doesn’t reload — it unloads and re-autoloads
This is the section worth reading even if you skip everything else. Ruby has no mechanism to update a class object in place and have every existing reference see the change. So “reloading” is really: remove the constant (Object.send(:remove_const, "User")), forget the file was ever loaded, and let the next reference to User autoload it fresh — as a different object.
irb> joe = User.newirb> reload!irb> alice = User.newirb> joe.class == alice.class=> falsejoe is still an instance of the original User class object. After reload!, the User constant points at a new class entirely. alice is an instance of that new one. Nothing connects them — joe’s class is simply stale, silently, with no error raised anywhere.
The subclassing case is the one that actually bites people in practice:
class VipUser < UserendIf lib isn’t an autoload path, VipUser never reloads. User does. After a reload, VipUser’s superclass is still the original User class object — not the one currently sitting in the User constant. Methods you added to User are missing from what VipUser actually inherits from; methods you deleted are still there. No exception. It just quietly behaves like the old code.
The guide gives two fixes, and they’re not interchangeable:
# Fix 1: stop holding the object, hold its name instead.config.user_model = "User"# ...and resolve it at the point of use:config.user_model.constantize# Fix 2: make the code non-reloadable so there's no new object to miss —# move VipUser out of lib and into app, so both classes reload together.Which one applies depends on who holds the reference. If it’s your own application code, move the class into app so everything reloads in lockstep. If something outside the reload cycle holds it — a middleware stack, a framework registry, an engine’s config — you can’t make that side reload, so you pass a name instead.
Warning
Notice the fix for VipUser < User is the reverse of the usual advice. The instinct is “classes
in lib are the problem, move them to app” — but the actual problem here is a non-reloadable
class inheriting from a reloadable one. The fix isn’t “make lib reloadable,” it’s “make the
inheritance chain reload together.”
The MyDecoration example, explained properly this time
Earlier versions of the guide gave this example one sentence. The rewrite walks through why it’s permanently broken if the module is reloadable:
initializer "decorate ActionController::Base" do ActionController::Base.include(MyDecoration)endinclude inserts whatever object MyDecoration currently refers to into ActionController::Base’s ancestor chain — and the chain holds that object directly, not the constant name. If MyDecoration were reloadable, a later reload would define a brand-new module and update the MyDecoration constant to point at it, but ActionController::Base’s ancestor chain would still hold the original module object. Every controller in your app would keep running the version loaded at boot. Your edits would compile, reload would report success, and nothing you changed would take effect — until you restarted the process. This is exactly why decorator modules like this belong in autoload_once_paths: not reloading isn’t a limitation here, it’s the only way include keeps working as edited.
Eager loading, and what “CoW-friendly” actually means
The old guide said eager loading is “CoW-friendly” and left it there. The rewrite spells out the mechanism: Zeitwerk::Loader.eager_load_all defines every constant up front, before a server forks its worker processes. Because the constants exist before the fork, forked workers share that already-loaded memory via the OS’s copy-on-write pages instead of each worker loading (and holding its own private copy of) the same classes. That’s the actual memory saving — it’s not just “loading everything early is probably faster.”
It’s also worth knowing eager_load_all, not just your own app’s loader, runs: it broadcasts eager_load to every Zeitwerk loader in the process, which means any gem dependency that manages its own code with Zeitwerk gets eager-loaded too, not only your application’s app directory.
Rails.autoloaders is Zeitwerk, not a Rails wrapper around Zeitwerk
The last rewritten section states something the old guide implied but never said directly: Rails doesn’t build its own configuration layer on top of Zeitwerk. Rails.autoloaders.main and Rails.autoloaders.once are the actual Zeitwerk loader objects, and anything Zeitwerk itself supports is available by calling it directly:
# Treat a directory as organizational rather than as a namespace.Rails.autoloaders.main.collapse("#{Rails.root}/app/models/shapes")
# Map an autoload path to a namespace other than Object.Rails.autoloaders.main.push_dir("#{Rails.root}/app/services", namespace: Services)
# Override how file names are converted to constant names.Rails.autoloaders.each do |autoloader| autoloader.inflector.inflect("html_parser" => "HTMLParser")endIf an engine needs to support both Rails 6’s classic mode and current zeitwerk mode, Rails.autoloaders.zeitwerk_enabled? tells you which one you’re in — and the rewrite notes it’s kept around in current Rails purely for that backward-compatible case, where it just returns true unconditionally.
None of this changes what ships in the next Rails release. What it changes is whether the one guide most Rails developers skim once and never reread actually explains why the rules are the rules — which, for a mechanism as easy to half-understand as autoloading, is most of the value.