Open-source project
boazsegev/iodine avatar
boazsegev/iodine

Iodine: a C-powered Ruby server that keeps its sockets out of the GVL

iodine - HTTP / WebSockets Server for Ruby with Pub/Sub support

988 stars55 forksCMIT

At a glance

What is it?
Iodine is an MIT-licensed Ruby server and client built on facil.io that runs Rack and NeoRack applications with client I/O handled by a C reactor thread outside the GVL, and ships WebSockets, SSE, Pub/Sub, static file serving and TLS in the same binary. The embedded TLS stack is unaudited, which is the caveat to read before production use.
Who is it for?
Iodine suits teams that already run a Rack or Rails app, want WebSockets, SSE and cross-worker Pub/Sub without adding Redis, and are willing to point the GVL-sensitive work at an unfamiliar binary server. It does not suit a team that needs an audited TLS stack, a message bus with independent operations, or a release cadence with published versions, since the project has no GitHub releases.
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 1 day ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Client I/O in a C reactor that the GVL cannot block

The central design claim of Iodine is that network I/O never waits on the Ruby GVL. Each worker runs its client I/O in a dedicated C reactor thread outside the GVL, while a separate pool of Ruby threads runs the application code. That split is the whole argument for the project: a pure-Ruby server ties socket handling to the same interpreter lock as the code handling requests, so a slow request can stall the reading of other connections. Iodine says this separation lets one worker keep thousands of connections alive while Ruby code executes, and the effect on a Ruby programmer is that the concurrency story is no longer something you tune with thread pool sizes alone. Iodine is built on facil.io and runs both Rack and NeoRack applications, and it also ships an evented client for TCP, HTTP, WebSockets and SSE, so the same library is usable as a client in tests and tooling. The README also claims hot application restarts.

The accept handoff that balances connections across workers

Load balancing here is not a separate proxy layer, it is a side effect of how connections are accepted. The master process handles worker monitoring and hosts the Pub/Sub postoffice, while workers run the reactor and the Ruby threads. Iodine calls accept for new connections only when Ruby threads are available, which automatically routes each new connection to the first worker with a free Ruby thread. The consequence for an operator is worth stating plainly: there is no separate balancing configuration to keep in sync with thread counts, and no chance of a worker accepting work it cannot execute. The cost is that the granularity of balancing is a thread, not a connection, so a worker with a long-running request holds that slot until the request returns. Iodine also honours RAILS_MAX_THREADS and WEB_CONCURRENCY when nothing else is configured, which is what lets most Rails applications run without an initializer, and it accepts THREADS and WORKERS environment variables for direct control.

Pub/Sub across workers, and the Redis or Valkey escape hatch

Iodine includes a Pub/Sub engine that routes messages across local worker processes, and the README frames this as a way to broadcast from a single-host deployment without deploying, monitoring, securing or paying for Redis, Valkey or another broker. When the deployment genuinely needs an external service, Iodine can talk to a RESP3-compatible one instead, configured on the command line:

bash
iodine -w -1 -t 8 -r redis://localhost:6379

The API reference covers programmatic RESP3 setup and direct commands, so the broker is not limited to a startup flag. There is a third mode, an optional LAN mesh, where machines on the same local network find each other through automatic UDP peer discovery. All participating machines must share the broadcast port and the secret:

bash
iodine -w -1 -t 8 -bp 7555 -scrt "my-cluster-secret"

Mesh traffic uses ChaCha20/Poly1305 with the shared secret, and the SECRET and PUBSUB_PORT environment variables set the same values. The README flags two honest weaknesses: the secret may be readable by other users on the same machine, and the feature is lightly tested, with the author asking for reports of dropped messages or isolated machines.

Static files answered in C, with pre-compressed variants

The static file layer runs without Rack middleware, and the C layer answers GET and HEAD requests directly, bypassing Ruby. Other methods and misses fall through to the application, so a dynamic route and a file in the public directory can coexist. The directory is given on the command line:

bash
iodine -www /my/public/folder

The lookup is more thorough than a plain file server. When the client accepts compression, Iodine serves a neighbouring pre-compressed .br, .zstd, .gz or .zip file rather than compressing on the fly, and it probes missing extensions such as .html, .htm, .txt and .md along with directory index files. Dynamic response deflation and WebSocket permessage-deflate are on by default and can be switched off with -no-dynd and -no-wsd, which is the setting to reach for when you are debugging a client that mishandles compressed frames. Request logging is available with -v, and the README notes that date and time strings are cached to cut formatting work on the hot path.

Two TLS backends, one of them unaudited

TLS is configured on the command line, and there are two backends. The OpenSSL backend is auto-detected at install time, and the embedded TLS 1.3 stack can be selected with a flag:

bash
iodine -cert cert.pem -key key.pem # OpenSSL backend
iodine -mtls # embedded TLS 1.3

You can switch backends at runtime with Iodine::TLS.default = :iodine or :openssl. The README is explicit that the embedded TLS implementation is unaudited and advises reviewing its suitability before production use, and that sentence is the single most important line in the security section of the documentation. Client certificate trust is configured in the binding URL:

bash
iodine -b https://0.0.0.0/?trust=ca.pem

The API reference covers per-listener Iodine::TLS#trust and peer certificate chain inspection. Iodine also sets defaults for HTTP header and body sizes and for incoming WebSocket message size, configurable with -maxln, -maxhd, -maxbd and -maxms, and large HTTP payloads can be diverted to temporary files rather than held in memory.

Running a Rack or Rails app under Iodine

For a Bundler-managed app, Iodine is a gem like any other. The Gemfile entry is:

ruby
# Gemfile
gem "iodine", "~> 0.8"

and the app starts with:

bash
bundle install
bundle exec iodine

Iodine loads config.ru, or config.nru for a NeoRack app, from the current directory by default. The README advises keeping your current server gem installed while you evaluate Iodine, which is a sensible way to keep a rollback path. For Rails applications that currently use Puma, the instruction is to remove config/puma.rb or make its content conditional when switching servers, since Iodine honours RAILS_MAX_THREADS and WEB_CONCURRENCY and most Rails apps then need no initializer. For a standalone Rack app, the CLI is installed directly:

bash
gem install iodine
iodine

The README also recommends command line options and environment variables over code for configuration changes, so that a config change does not require a redeploy of application code.

What to check before the switch, and what the project does not publish

Two things deserve attention before Iodine carries production traffic. The first is the security limits, and the README points at iodine -h for the available settings and advises load testing the chosen limits and timeouts, because the defaults for header, body and WebSocket message sizes are conservative by design and large payloads can end up in temporary files. The second is the difference from the server you are replacing. Puma is the baseline the README measures against, and the README claims roughly 2.4 times Puma's throughput at lower latency in a local three-trial benchmark, which is the project's own measurement on its own hardware rather than a result anyone else has confirmed. The architectural difference is that Iodine moves socket handling into a C reactor outside the GVL and folds WebSockets, SSE, Pub/Sub, static files and TLS into the same binary, where Puma's configuration lives in config/puma.rb. On the licensing side, Iodine is MIT, so using it commercially carries no copyleft obligation. The repository has no GitHub releases, so there is no published version history to diff against, although the last push was on 2026-10-14 and a CHANGELOG.md is part of the tree.

Editorial conclusion

Iodine suits teams that already run a Rack or Rails app, want WebSockets, SSE and cross-worker Pub/Sub without adding Redis, and are willing to point the GVL-sensitive work at an unfamiliar binary server. It does not suit a team that needs an audited TLS stack, a message bus with independent operations, or a release cadence with published versions, since the project has no GitHub releases. Before switching a Rails app over, remove config/puma.rb, check the header and body size limits with iodine -h, and decide whether the embedded TLS implementation or OpenSSL is the backend you want.

Frequently asked questions

What is Iodine and what does it replace?

Iodine is a C-powered server and client for Ruby, built on facil.io, that runs Rack and NeoRack applications. It handles client I/O in a C reactor thread outside the Ruby GVL and includes WebSockets, SSE, Pub/Sub, static files and TLS, so it can replace a Puma deployment.

How do I run a Rails app with Iodine?

Remove config/puma.rb or make its content conditional when switching servers, then start Iodine with bundle exec iodine. Iodine honours RAILS_MAX_THREADS and WEB_CONCURRENCY when they are not otherwise configured, so most Rails apps need no initializer.

Does Iodine need Redis for Pub/Sub?

No. It has a built-in Pub/Sub engine that routes messages across local worker processes, and it can also point at a RESP3-compatible service such as Redis or Valkey when the deployment needs an external broker.

Is the TLS implementation in Iodine safe for production?

Iodine offers an OpenSSL backend, auto-detected at install time, and an embedded TLS 1.3 stack selected with -mtls. The README states that the embedded implementation is unaudited and advises reviewing its suitability before production use.

How do I set size limits for headers, bodies and WebSocket messages?

Iodine sets defaults for HTTP header and body sizes and for incoming WebSocket message size, and they are configurable with -maxln, -maxhd, -maxbd and -maxms. The README advises checking iodine -h and load testing the limits you choose.

Official sources

  1. boazsegev/iodine on GitHub
  2. Issues
  3. License: MIT
  4. README
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/boazsegev-iodine.svg)](https://hysenlabs.com/projects/boazsegev-iodine)

Was this useful?