Open-source project
contribsys/einhorn avatar
contribsys/einhorn

Einhorn shares listening sockets across workers and reloads them in place

Einhorn: the language-independent shared socket manager

1,645 stars47 forksRubyMIT

At a glance

What is it?
Einhorn is a Ruby shared socket manager that binds listening sockets in a master process, hands them to workers through environment variables and reloads worker code by signal. It suits teams running long-lived servers in any language, and it is a Unix-only tool with a command socket that parses YAML.
Who is it for?
Adopt Einhorn if you run a long-lived server that binds a fixed port, you want several workers behind one socket, and you need code reloads that do not drop the listening socket. Do not adopt it if your application already has its own master process, if you are on a platform without Unix sockets and file descriptor passing, or if you are unwilling to treat the command socket as a trusted interface.
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 71 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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The master binds the port, the workers only inherit the descriptor

Einhorn is described as the language-independent shared socket manager. It makes it easy to run and keep alive multiple copies of a single long-lived process, and if that process is a server listening on a socket, Einhorn opens the socket in the master process so that it is shared among the workers.

That is the whole mechanism, and it is the reason the tool is called language-independent. The application does not have to know anything about the socket manager's internals, only that a file descriptor is already open and listening. There is no proxy in the data path, no port translation, and no per-worker bind.

The design goal is stated plainly: Einhorn is designed to be compatible with arbitrary languages and frameworks, requiring minimal modification of your application, and it is simple to configure and run. It also has no dependencies outside of the Ruby standard library, which was done specifically to maximise compatibility, so the master process does not drag a gem tree into your deployment.

The installation is a plain gem install:

bash
gem install einhorn

Or you can build the gem from source with gem build einhorn.gemspec and then install the built gem.

The usage shape is one command:

bash
einhorn [options] program

Einhorn will open one or more shared sockets and run multiple copies of your process, and the README says you can seamlessly reload your code, dynamically reconfigure Einhorn, and more.

Workers learn their sockets through EINHORN_FD_0 and EINHORN_FD_COUNT

The mechanism for telling a worker which socket to use is environment variables, and the README is precise about the format. The opened file descriptors are represented as file descriptor numbers in a series of environment variables named EINHORN_FD_0, EINHORN_FD_1 and so on, respecting the order the -b options were provided in, with the total number of file descriptors in EINHORN_FD_COUNT.

A single bound socket looks like this:

bash
EINHORN_FD_0="6" # 127.0.0.1:1234
EINHORN_FD_COUNT="1"

Two sockets look like this:

bash
EINHORN_FD_0="6" # 127.0.0.1:1234,r
EINHORN_FD_1="7" # 127.0.0.1:1235
EINHORN_FD_COUNT="2"

Addresses are passed with -b ADDR, and the syntax is ADDR := (IP:PORT)[<,OPT>...]. Two options are valid: r, so_reuseaddr, which sets SO_REUSEADDR on the server socket, and n, o_nonblock, which sets O_NONBLOCK. The comma separated form means one -b argument can carry several options, for example -b 127.0.0.1:1234,r.

The upstream example shows the whole thing in one line:

bash
einhorn -b 127.0.0.1:2345,r -m manual -n 4 -- example/time_server

That runs four copies of the process with EINHORN_FD_0=6 and EINHORN_FD_COUNT=1, where file descriptor 6 is a server socket bound to 127.0.0.1:2345 with SO_REUSEADDR set. The README is explicit that it is then your application's job to figure out how to accept() on that descriptor. Einhorn does the binding and the hand-off; the accept loop stays yours.

Two ACK mechanisms decide when a worker counts as healthy

After Einhorn spawns a worker, it will only consider the worker up once it has received an ACK. Two mechanisms are supported, manual and timer, and the difference between them is the difference between a server that refuses traffic until it is ready and one that accepts traffic and then fails.

Manual ACK is configured with -m manual and requires your application to send a command to the command socket once it is ready. The README calls this the safest ACK mechanism. In Ruby, the worker code is two lines:

ruby
require 'einhorn/worker'
Einhorn::Worker.ack!

If you are writing in a different language, or you do not want Einhorn in your namespace, you send the string {"command":"worker:ack", "pid":PID} to the UNIX socket pointed to by EINHORN_SOCK_PATH, with a trailing newline. There is an even easier route: pass -g to Einhorn and you just write() that message to the open file descriptor pointed to by EINHORN_SOCK_FD, so the worker never has to know where the socket lives on disk.

The timer ACK is the default and uses a one second timeout. If your process has not exited after one second, it is considered ACK'd and healthy. You can modify the timeout to suit your application, and the README says you can even set it to 0, which you would do by passing -m FLOAT.

The safety difference is real. A timer ACK of one second means a worker that crashes on startup within that window is treated as up, while a worker that hangs after binding the socket is also treated as up, since the timer measures the process not having exited rather than the application being ready. A manual ACK ties the readiness signal to the moment your server actually starts serving.

Seamless upgrade works by spawning new workers then sending SIGUSR2

The reload story is the part that sets Einhorn apart from a plain forking supervisor, and the sequence is deliberate.

You cause your code to be reloaded by upgrading the worker code on disk and running the upgrade command through einhornsh. Once the new workers have been spawned, Einhorn sends each old worker a SIGUSR2, and SIGUSR2 should be interpreted as a request for a graceful shutdown.

The order is the point. New workers start first and inherit the same listening socket, so there is no window where nothing is accepting. Only when the new workers are running does the old generation get the signal to drain and exit. Because the socket was never closed, connections in flight are not refused during the switch.

The catch is that SIGUSR2 handling is your responsibility. Your application has to install a handler that treats the signal as a graceful shutdown, and the README describes it as what it should be interpreted as. A program that does not trap SIGUSR2 will simply be terminated by the default disposition, which is a crash rather than a drain, and the reload would then behave like a kill.

For Ruby applications there is a further option. Einhorn can optionally preload its code, so it only has to load the code once per upgrade rather than once per worker process. The README says this also saves on memory overhead, since all the code in these processes is stored only once using the operating system's copy-on-write features. You enable it with -p PATH_TO_CODE, and you have to define an einhorn_main method.

The command socket speaks YAML, and that is the security boundary

Einhorn opens a UNIX socket to which you can send commands, and einhornsh is the client for it. The README states that Einhorn relies on file permissions to ensure that no malicious users can gain access, and that you can run with a -d DIRECTORY to change the directory where the socket will live.

The security note is the important part, and the README is blunt about it: the command socket uses a line-oriented YAML protocol, and you should ensure you trust clients to send arbitrary YAML messages into your process.

That is a real constraint. A YAML parser in the master process, fed by whoever can open the socket, is a privileged input surface. File permissions are the only protection described, so the socket's directory and mode are the whole security model. A deployment that puts the socket in a shared directory, or that runs several users against the same host, has to think about this before exposing the port that the workers bind.

The administrative surface includes the upgrade command described earlier, and the help command lists what else is available. The -e PIDFILE option says where to write the Einhorn pidfile, and -f LOCKFILE where to store the Einhorn lockfile, which is how a second instance refuses to start on the same setup.

The re-exec option is aimed at deploy tooling. With --reexec-as you can replace the einhorn command with a command or script of your own, and the README gives a bash example for a Capistrano-like deploy process with changing symlinks, so that the process follows the symlink to the current release directory.

MIT licensed, version 1.0 since 2022, with the last push in August 2026

The licence is MIT, with LICENSE.txt in the repository as the authoritative text, and this is a licensing statement rather than legal advice.

The version history is where a prospective adopter should pause. The most recent release is v1.0.0, tagged on 2022-07-14, and before that 0.8.2 from 2020-10-29. The repository is not archived, and the last push was on 2026-08-13, so the code has moved since the 1.0 release, but the released version line has not been cut in four years.

That is not automatically a problem. Einhorn's job is narrow, its interface is a command line and a socket protocol, and the pieces it depends on, Unix sockets, file descriptor passing, SIGUSR2, copy-on-write, are stable operating system features rather than moving targets. A tool like this ages better than one that wraps a fast-moving framework.

The maintenance signals are still worth weighing. The repository uses.travis.yml for CI and a.standard.yml, which is the config for the standard Ruby linter, plus a Rakefile, a Gemfile and Changes.md for the changelog. The example directory holds pool_worker.rb, thin_example and time_server, and the test directory holds the test suite.

The name of the repository organisation is contribsys, and the project is a good example of a small Unix primitive that solves one problem completely and then stops growing, which is exactly why a four-year-old release number is not a red flag on its own.

Editorial conclusion

Adopt Einhorn if you run a long-lived server that binds a fixed port, you want several workers behind one socket, and you need code reloads that do not drop the listening socket. Do not adopt it if your application already has its own master process, if you are on a platform without Unix sockets and file descriptor passing, or if you are unwilling to treat the command socket as a trusted interface. Before you deploy, decide between manual ACK and the default one second timer ACK, and read the line-oriented YAML protocol warning in the README, because anything that can reach the command socket can send arbitrary commands to the master process.

Frequently asked questions

How do I install and run Einhorn?

Install it with gem install einhorn, or build it with gem build einhorn.gemspec and install the built gem. The usage is einhorn [options] program, and einhorn -h shows the detailed usage.

How does a worker process find the socket it should listen on?

Einhorn passes the opened file descriptors through environment variables named EINHORN_FD_0, EINHORN_FD_1 and so on, in the order the -b options were given, with the total in EINHORN_FD_COUNT. The README says it is then your application's job to accept() on that descriptor.

What is the difference between manual ACK and timer ACK in Einhorn?

A manual ACK, configured with -m manual, requires the application to send a worker:ack command to the command socket once it is ready, and the README calls it the safest mechanism. The default timer ACK treats a worker as up if it has not exited after one second, and the timeout can be changed with -m FLOAT.

How does Einhorn reload worker code without dropping the socket?

You upgrade the code on disk and run the upgrade command through einhornsh. Once the new workers have been spawned, Einhorn sends each old worker a SIGUSR2, which the application should interpret as a request for a graceful shutdown.

Is the Einhorn command socket safe to expose?

The README notes that Einhorn relies on file permissions to ensure no malicious users can gain access, and warns that the command socket uses a line-oriented YAML protocol, so you should ensure you trust clients to send arbitrary YAML messages into the process.

Official sources

  1. contribsys/einhorn on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/contribsys-einhorn.svg)](https://hysenlabs.com/projects/contribsys-einhorn)

Was this useful?