Technical correction, October 2026: This post originally proposed sharing PostgreSQL connections across a fork. That section has been corrected below. The shared-supervisor idea remains an untested proposal.
Every Rails developer knows the feeling. You type bin/rspec, hit enter, and wait. You wait for Bundler. You wait for Rails to boot. You wait for ActiveRecord to connect. Five seconds later, your test runs in 10 milliseconds.
To solve this, we have preloaders like Spring. They load the app into memory once and then fork a process for each command. It's the difference between a 5-second feedback loop and a 0.5-second one.
But everyone also knows the other feeling. The "weird failure" feeling. The "thread didn't start in 5 seconds" error. The phantom DB connection that thinks it's locked. The dreaded advice to "just kill Spring processes" when things get wonky. Spring has become a necessary evil. We hate the complexity, but we can't live without the speed.
Forking a preloaded Rails app means dealing with database connections, background threads, file watchers, and libraries that each have their own lifecycle. That is a difficult systems problem.
It's time we stop treating development tools as second-class citizens. We need a new architecture for preloaders. We need to apply the same rigorous engineering principles we use in production to our development workflows.
Maybe we need a "Grand Unified Supervisor"?
The Problem: The Great Divide
Right now, the Rails ecosystem is split by a massive, artificial divide.
On one side, we have Production Process Managers: Puma, Unicorn, Falcon. These are battle-hardened tools designed to keep apps alive under heavy load. They handle signal trapping, graceful shutdowns, worker supervision, and complex threading models. Their goal is Stability.
On the other side, we have Development Process Managers: Spring (and its predecessors, Zeus and Spork). They are designed to provide a fast, interactive experience. Their goal is Freshness.
There is overlap under the hood: both can preload an application and fork worker processes. Their requirements differ, though. A web server needs to handle concurrent requests; a development preloader also needs code reloading and an interactive command lifecycle.
Puma in cluster mode can preload the app before forking web workers. Spring preloads it before forking command processes. That shared mechanism makes me wonder whether some lifecycle code could be shared too.
Their separate configuration files and lifecycle hooks reflect those different jobs. I would like to explore which parts of process supervision could be shared without making either tool harder to use.
We don't need two different ways to manage processes. We need one core supervisor, with different plugins for different contexts.
A Proposal: Explicit Resource Ownership
One source of preloader failures is inherited process state. After a fork, a child can retain resources or library state that depended on threads which no longer exist in that child. This can cause deadlocks or broken connections, but it does not explain every Spring failure.
A better preloader would need explicit rules for resources before and after each fork, plus tests for the adapters and libraries it supports. Here is the architecture I would like to investigate.
1. The Aggressive Stance: Sanitize threads
The primary job of the Supervisor is to maintain a "Warm Parent Process." Before it ever allows a fork, this parent process must be vigorously sanitized.
Only the thread that calls fork survives in the child. Other threads do not continue running there, although the child inherits a snapshot of memory and open file descriptors. That inherited state may need explicit cleanup or reinitialization.
Before forking, the supervisor would need to coordinate with libraries that start background work, using supported shutdown and fork hooks. The resources to account for include:
- Rails' own file watchers: The app should not know it's being watched.
- ActiveRecord connection reapers.
- APM background threads (NewRelic, Datadog, etc.).
- Global thread pools (like concurrent-ruby).
The parent process must be dormant before it clones itself.
2. Separate Database Connections in Each Process
A fast development loop still needs reliable resource ownership. Parent and child processes should not reuse the same inherited PostgreSQL connection.
The libpq documentation warns against forking with an open connection: the processes share sockets and other operating-system resources, which can produce unpredictable results. Simply taking turns using that connection does not make the arrangement safe.
- Before the fork: use the database adapter’s supported lifecycle to disconnect connections and stop background work that cannot safely cross the fork.
- In the child: establish its own database connections and initialize the resources needed for the command.
- On exit: clean up the child’s resources. The parent must not rely on rolling back or reclaiming a database session the child used.
The original version of this post proposed sharing an inherited connection through a serial lease and called that “100% safe.” That claim was wrong. Reconnecting has a cost, but avoiding it would require a design explicitly supported by the database client and protocol, with evidence that it works.
3. The File Watcher Inversion
Another idea is to keep filesystem watching outside the process that preloads Rails, so watcher threads do not need to cross the fork boundary.
The supervisor could notify the warm parent when files change. The parent would then coordinate reloading through Rails’ reloader at a safe point, outside a signal handler and without overlapping a command or fork.
This could reduce the amount of threaded state in the preloaded app. It would still need testing around reloads, concurrent commands, and worker shutdown.
The Future: Convergence
Existing servers offer useful ideas to study. Shopify’s Pitchfork, for example, uses reforking. That does not make it a drop-in CLI preloader, but its lifecycle design is relevant to the proposal.
Puma and ServerEngine are also worth studying. Setting workers 0 in Puma selects single-process mode; it does not provide a mechanism for forking arbitrary CLI commands. A shared supervisor would need a purpose-built command adapter.
I would like to see an experiment with a shared supervisor layer for fork hooks, resource ownership, and worker lifecycle. Web and CLI adapters could then build on it. This is a proposal, not an implementation or a demonstrated replacement for Spring or Puma.
The test would be whether it makes development faster and more reliable without adding more complexity than it removes.