zeitwerk: Ruby autoloading where file paths are the constant map
Efficient and thread-safe code loader for Ruby
At a glance
- What is it?
- fxn's loader replaces require statements with a naming convention, resolving MyGem::Woo::Zoo from lib/my_gem/woo/zoo.rb on first reference, with optional eager loading, reloading and per-loader inflection.
- Who is it for?
- Adopt zeitwerk if your Ruby project or gem already keeps class files in a tree that mirrors its namespace, since the whole gem is the payoff for that discipline and the README states the mapping without hedging. Skip it if your files do not follow the convention, or if you depend on load order side effects, because a loader that resolves by name cannot express them.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 38 days ago.
- What is it written in?
- Mainly Ruby, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem it names: require statements you have to remember
Zeitwerk is described in one line as an efficient and thread-safe code loader for Ruby, and the second sentence is the interesting one: given a conventional file structure, it loads your project's classes and modules on demand or upfront, and you do not write require calls for your own files.
That is the entire pitch, and the README is unusually honest about where it came from. Its motivation section has subsections titled Kernel#require is brittle and Rails autoloading was brittle, which tells you this is a response to two specific pieces of Ruby history rather than a general rethinking of loading. Ruby's own require is path-based, so the file name and the constant name are free to disagree, and a project ends up with a map from paths to constants maintained by hand. Rails tried to replace that with autoloading derived from names, and the breakage that followed pushed a generation of convention-based loaders into separate gems.
The audience is anyone writing a Ruby gem or application that has outgrown its require list. The repository is a library plus its own tooling: `zeitwerk.gemspec`, a `lib/` directory, a `test/` directory, a `bin/` directory and an `extras/` directory, with CI wired up through GitHub Actions and a RuboCop configuration. Zero open issues and 2,141 stars at the time of writing are context, not a recommendation.
The mechanism: one mapping from directory names to constant names
The core idea is stated as a heading in the README: file paths match constant paths. A directory and its files are named after the classes and modules they define, and the loader derives everything else from that.
lib/my_gem.rb -> MyGem
lib/my_gem/foo.rb -> MyGem::Foo
lib/my_gem/bar_baz.rb -> MyGem::BarBaz
lib/my_gem/woo/zoo.rb -> MyGem::Woo::ZooTwo consequences follow. Because the mapping is derived rather than declared, a typo in a file name becomes a missing constant at reference time rather than a load error at boot, which is a failure you find in tests instead of in production. And because nothing is registered up front, the cost is paid only when a constant is first mentioned.
The README notes two implementation details that explain why it is fast. Zeitwerk issues require calls using absolute file names exclusively, so there are no file system lookups against $LOAD_PATH, and the directories it manages do not even need to be on $LOAD_PATH. It also scans the project tree at most once, descending into subdirectories lazily, only when their namespaces are actually used. A large lib/ tree with a long tail of unused files costs almost nothing at boot.
Directories become namespaces implicitly, and the documentation has a dedicated subsection for implicit namespaces alongside explicit namespaces, which lets a module own more than its directory name, defined either in an ordinary Ruby file or in an nsfiles-style entry. There is also a section on collapsing directories for when one level of the tree carries no constant of its own.
Setting up a gem in four calls
The synopsis for gems is short enough to read as the whole setup. In your main file under lib/, require the gem, build the loader, and call setup:
# lib/my_gem.rb (main file)
require 'zeitwerk'
loader = Zeitwerk::Loader.for_gem
loader.setup # ready!
module MyGem
# ...
end
loader.eager_load # optionallyRead the order carefully. `setup` comes before your module body, because setup installs the autoload hooks that will resolve the constants your module body refers to. Move it after and the first constant reference inside the module definition triggers a load that has not been armed yet.
The generic interface is the same shape with an explicit directory, which is what you want in an application or when one process hosts several independent projects:
loader = Zeitwerk::Loader.new
loader.push_dir(...)
loader.setup # ready!`for_gem` is described as the main interface for gems, and the README also documents `for_gem_extension`, for a gem that extends another gem's namespace rather than owning its own.
One detail worth knowing because it looks like a leak: the loader variable is allowed to go out of scope. Zeitwerk maintains a registry of all loaders, so the object is not garbage collected out from under the autoload hooks it installed. In a Rails application the framework holds the reference; in a script you can let the local variable disappear.
Autoload, eager load, reload, and the order they happen in
Autoloading and eager loading are the two ends of the same switch. Eager loading pulls in everything the tree can define, which trades startup time for the certainty that every file parses. Broadcasting it to every loader in the process takes one call:
loader.eager_loadZeitwerk::Loader.eager_load_allThe documentation treats exclusions as a first-class feature rather than an afterthought, with subsections for excluding individual files, whole directories, entire namespaces, namespaces shared by several loaders, and a global eager load across the process. Shared namespaces matter more than they sound: when two loaders both manage a piece of the tree, one of them can take responsibility for eager loading it and the other can defer.
Reloading is opt-in, and the opt-in has to happen before setup, which the code comment points out directly:
loader = Zeitwerk::Loader.new
loader.push_dir(...)
loader.enable_reloading # you need to opt-in before setup
loader.setup
...
loader.reloadThe README is careful about the cost of this. Reloading is useful during web application development, but coordination is required to reload in a thread-safe manner, and there is a dedicated Thread-safety subsection under the reloading configuration to explain how. In practice that means reloading has to be driven from a single request boundary rather than from whatever thread happens to need fresh code.
Reloading also explains the on_unload callback in the callback set. The documented hooks are on_setup, on_load and on_unload, and on_unload exists specifically so a listener can release what on_load acquired. There is a Technical details subsection underneath, which is where the exact ordering of those callbacks relative to constant assignment is specified.
Where the convention gets in the way
A loader that derives constants from names has no way to express a file that breaks the pattern, and the README gives that problem its own heading: shadowed files. Two files claiming the same constant path is undefined behaviour, and the only fix is renaming one of them, which is disruptive on a large tree.
The escape hatch is the ignore API, and the documentation frames it as a set of use cases rather than a general mechanism. Files that do not follow the conventions get ignored and required by hand. The adapter pattern gets one, where a third-party library owns the namespace and you keep your own code out of its tree. Test files mixed in with implementation files get ignored so that eager loading in production does not pull in your specs.
The other named failure mode is circular dependencies, and the documentation's phrasing is blunt: beware of circular dependencies. Name-derived loading makes a circular reference a load-time deadlock or a partially defined constant rather than a boot error, and the fix is a design change in your own code, not a loader setting.
Two more limits are documented. There is a section on reopening third-party namespaces, which is the supported way to extend a constant you do not own without loading its file yourself and without confusing the loader. And there is a section on encodings, because a file whose bytes are not what Ruby expects will parse differently depending on how it was required.
Inflection, introspection and the parts you can check from the outside
CamelCase is a default, not a law. The inflector API has four documented implementations: `Zeitwerk::Inflector`, which is the standard one; `Zeitwerk::GemInflector`, which applies the gem naming convention; `Zeitwerk::NullInflector`, which derives constant names mechanically from the file name; and a custom inflector interface for anything else. Setting `Zeitwerk::Inflector.inflector` on the default class changes the mapping process-wide, which is the knob to reach for when your project has a house naming style.
Introspection is what makes compliance testable rather than aspirational. The README documents `Zeitwerk::Loader#dirs` for the directories a loader manages, `Zeitwerk::Loader#cpath_expected_at` for the constant path a loader expects to find at a given path, and `Zeitwerk::Loader#all_expected_cpaths` for the full list. The last one is the one to use in a test: compare the expected constant paths against what your suite actually references and the difference is your unused files, which is otherwise invisible.
Debugging output is configured through logging rather than by printing. The README has a Loader tag subsection, which is how you point a logger at one loader among several when you need to see what a specific tree resolves. There is also a Testing section describing how the gem verifies its own convention compliance, and a Rules of thumb section that collects the advice the author would otherwise repeat in every issue thread.
Editorial conclusion
Adopt zeitwerk if your Ruby project or gem already keeps class files in a tree that mirrors its namespace, since the whole gem is the payoff for that discipline and the README states the mapping without hedging. Skip it if your files do not follow the convention, or if you depend on load order side effects, because a loader that resolves by name cannot express them. Check first that no two files claim the same constant path, and run loader.eager_load in a test so the tree is exercised without a request coming in to trigger it. Under the MIT license the code carries no copyleft obligation, and the last push on 2026-08-30 means an upgrade lands new behaviour rather than only fixes.
Frequently asked questions
What is zeitwerk in Rails?
Zeitwerk is the code loader Rails 7 and later use by default in place of the older constant-based autoloading. It derives constants from file paths, so app/models/user.rb defines User, and it replaces the explicit require calls a Rails application used to carry in config/application.rb and the environment files.
Do I still need require statements for my own Ruby files?
No. The README states it directly: given a conventional file structure, Zeitwerk loads your classes and modules on demand or upfront, and you do not need to write require calls for your own files. What you still need is the naming convention, since the mapping from path to constant is what replaces the requires.
How do I set up Zeitwerk in a gem?
In your main file under lib/, require 'zeitwerk', call Zeitwerk::Loader.for_gem, then call loader.setup before the module body, and optionally call loader.eager_load at the end. The loader object does not need to be kept in a variable, because Zeitwerk holds a registry of all loaders.
Can Zeitwerk reload code during development?
Yes, with two conditions. You must call enable_reloading before setup, and the README notes that coordination is required to reload in a thread-safe manner, which is why the documentation has a dedicated Thread-safety subsection for it. Reloading is intended for web application development rather than production.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/fxn-zeitwerk)