Raptor
Raptor is a high-performance, preloading, pre-forking, multi-threaded Ruby 4+ web server implementing Rack 3.2+, using NIO for non-blocking I/O and Ractors for parallel HTTP/1.1 and HTTP/2 parsing via native C extensions, which also implement HPACK compression.
[!NOTE] Your application does not need to be Ractor-safe. Ractors handle protocol-level work only; your Rack application is invoked on a thread pool, so any thread-safe Rack app (including Rails) works as-is.
Reference documentation is published at https://joshuay03.github.io/raptor.
Installation
Install the gem and add to the application's Gemfile by executing:
bundle add raptor
If bundler is not being used to manage dependencies, install the gem by executing:
gem install raptor
Usage
# hello_world.ru
# frozen_string_literal: true
run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
> bundle exec raptor -w 10 -t 3 hello_world.ru
[Raptor 72876|Main|Main] Cluster initializing:
[Raptor 72876|Main|Main] ├─ Version: 0.22.3
[Raptor 72876|Main|Main] ├─ Ruby Version: ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23]
[Raptor 72876|Main|Main] ├─ Environment: development
[Raptor 72876|Main|Main] ├─ Master PID: 72876
[Raptor 72876|Main|Main] │ └─ 10 worker processes
[Raptor 72876|Main|Main] │ ├─ 1 server thread
[Raptor 72876|Main|Main] │ ├─ 1 reactor thread
[Raptor 72876|Main|Main] │ ├─ 1 HTTP/1.1 pipeline ractor
[Raptor 72876|Main|Main] │ ├─ 1 pipeline collector thread
[Raptor 72876|Main|Main] │ ├─ 3 worker threads (scaling, no limit)
[Raptor 72876|Main|Main] │ └─ 1 stats thread
[Raptor 72876|Main|Main] └─ Listening on 0.0.0.0:9292
[Raptor 72884|Main|Main] Worker 0 booted
[Raptor 72885|Main|Main] Worker 1 booted
[Raptor 72886|Main|Main] Worker 2 booted
[Raptor 72887|Main|Main] Worker 3 booted
[Raptor 72891|Main|Main] Worker 7 booted
[Raptor 72888|Main|Main] Worker 4 booted
[Raptor 72890|Main|Main] Worker 6 booted
[Raptor 72889|Main|Main] Worker 5 booted
[Raptor 72892|Main|Main] Worker 8 booted
[Raptor 72893|Main|Main] Worker 9 booted
> curl localhost:9292
Hello, World!%
Also works with rackup and rails server:
> bundle exec rackup -s raptor hello_world.ru
> bundle exec rails server -u raptor
Rails apps using SOLID_QUEUE_IN_PUMA run jobs through config/puma.rb, which Raptor does not load. Run bin/jobs as
a separately supervised process or container instead.
Configuration
Raptor reads options from a Ruby config file, environment variables, and command line flags, in increasing order of
precedence. Run bundle exec raptor --help for the full flag list.
The config file evaluates to a hash of options. By default Raptor loads raptor.rb then config/raptor.rb from the
working directory. Pass -c PATH to point at a specific file.
# raptor.rb
# Every key below is set to its default value; only include the ones you want to override.
{
binds: ["tcp://0.0.0.0:9292"],
socket_backlog: 1024,
drain_accept_queue: false,
workers: 4, # `Etc.nprocessors`
threads: 3,
max_threads: Float::INFINITY, # set to `threads` for a fixed pool
cpu_affinity: false,
clean_thread_locals: true,
clean_fiber_locals: false,
chdir: nil,
environment: nil, # falls back to `RAILS_ENV`, then `RACK_ENV`, then `"development"`
connection: {
first_data_timeout: 30,
chunk_data_timeout: 10,
write_timeout: 5,
max_body_size: nil,
body_spool_threshold: 1024 * 1024,
},
http1: {
ractors: nil,
persistent_data_timeout: 65,
max_keepalive_requests: 1000,
},
http2: {
ractors: nil,
max_concurrent_streams: 100,
keepalive_interval: 10,
keepalive_timeout: 5,
},
worker_boot_timeout: 60,
worker_timeout: 60,
worker_drain_timeout: 25,
worker_shutdown_timeout: 30,
refork_after: 1000, # `nil` on non-Linux
before_fork: [],
before_worker_boot: [],
before_worker_shutdown: [],
before_refork: [],
stats_file: "tmp/raptor.json",
control_url: nil,
pid_file: nil,
stdout_file: nil,
stderr_file: nil,
access_log_file: nil,
}
RAPTOR_WORKERS, RAPTOR_THREADS, and RAPTOR_MAX_THREADS set the corresponding options without a config file.
By default each worker adds threads beyond threads only while queued requests are waiting on blocking work such as
database or network calls. It stops adding them once Ruby execution becomes the bottleneck, and the extra threads
retire when the queue is idle. Set max_threads to cap this growth, or set it to threads for a fixed pool.
Bindings
binds accepts any combination of these URIs.
tcp://host:portfor TCP.localhostbinds both IPv4 and IPv6 loopback addresses.unix:///path/to/socketfor a Unix domain socket.ssl://host:port?cert=/path/to.crt&key=/path/to.keyfor TLS, negotiating HTTP/1.1 or HTTP/2 via ALPN.h2c://host:portfor cleartext HTTP/2.
Signals
Send to the master process.
| Signal | Effect |
|---|---|
INT |
Graceful shutdown |
TERM |
Graceful shutdown |
HUP |
Reopen stdout_file, stderr_file, and access_log_file |
USR1 |
Phased restart (rolling worker replacement) |
USR2 |
Hot restart (restart master, keeping listening sockets) |
A phased restart replaces workers one at a time while the master keeps running, picking up application code changes. A hot restart starts a new master with the original command line and the same listening sockets, picking up changes to configuration, dependencies, or Raptor itself.
systemd
Raptor supports socket activation and sd_notify, so it works with Type=notify units. When the socket unit is
active, Raptor serves the listening sockets systemd passes in place of binds.
# /etc/systemd/system/myapp.socket
[Socket]
ListenStream=0.0.0.0:9292
[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Service]
Type=notify
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/bundle exec raptor
ExecReload=/bin/kill -USR2 $MAINPID
KillMode=mixed
Stats
Workers publish their stats to stats_file, which defaults to tmp/raptor.json.
> bundle exec raptor stats
Master PID: 91348
Worker 0 (phase 0): pid=91350, requests=1234, busy=2/3, backlog=0, booted, last_checkin=10:42:01
Worker 1 (phase 0): pid=91351, requests=1199, busy=1/3, backlog=0, booted, last_checkin=10:42:01
...
Set control_url to a Unix socket URL such as unix:///tmp/raptor-control.sock to serve the same stats over
GET /stats.
(Micro) Benchmarks
Raptor 0.22.3 vs Puma 8.0.2 vs Falcon 0.57.0 across two workload profiles. IO-bound is a GET endpoint that interleaves 5-10 short sleeps (total 2.5-15ms) with small CPU work, simulating a read path that makes several DB or cache calls. CPU-bound is a POST endpoint that accepts a small JSON body, interleaves 3-5 chunks of JSON item building (total 450-1500 items) with sub-100µs sleeps, and returns the built array, simulating a write path that does most of its work in Ruby with a few near-zero-cost cache hits.
Raptor is run in two modes: Fixed keeps 3 application threads per worker, matching Puma, while Scaling starts with 3 and may add threads without a fixed limit when queued work is blocked outside the GVL. Both modes are compared with both Puma and Falcon in the table below. Raptor request-local cleanup and Puma's Fiber-per-request mode are disabled, and both threaded servers allow 999 requests per HTTP/1.1 keep-alive connection.
Each cell reports the median throughput and median p95 latency independently across 3 runs, so the two numbers in a row may come from different runs. Every run starts a fresh server process so the samples are independent of each other; state accumulated in a previous run cannot bias the next. Across the whole table, the widest spread ((max - min) / 2 / median) between runs of a single cell was ±19.6% for throughput and ±13.6% for p95.
| Protocol | Workload | Raptor mode | Raptor req/s | Raptor p95 | Puma req/s | Puma p95 | vs Puma req/s | vs Puma p95 | Falcon req/s | Falcon p95 | vs Falcon req/s | vs Falcon p95 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| HTTP/1.1 | IO | Fixed | 2.95k req/s | 80.20 ms | 1.55k req/s | 122.60 ms | 90.1% higher | 34.6% lower | 12.24k req/s | 14.00 ms | 75.9% lower | 472.9% higher |
| HTTP/1.1 | IO | Scaling | 7.05k req/s | 30.40 ms | 1.55k req/s | 122.60 ms | 354.1% higher | 75.2% lower | 12.24k req/s | 14.00 ms | 42.4% lower | 117.1% higher |
| HTTP/1.1 | CPU | Fixed | 7.29k req/s | 35.00 ms | 8.73k req/s | 20.30 ms | 16.4% lower | 72.4% higher | 6.71k req/s | 26.80 ms | 8.7% higher | 30.6% higher |
| HTTP/1.1 | CPU | Scaling | 6.73k req/s | 36.90 ms | 8.73k req/s | 20.30 ms | 22.8% lower | 81.8% higher | 6.71k req/s | 26.80 ms | 0.4% higher | 37.7% higher |
| HTTP/1.1 (keep-alive) | IO | Fixed | 2.52k req/s | 70.70 ms | 1.50k req/s | 102.60 ms | 67.8% higher | 31.1% lower | 6.23k req/s | 28.20 ms | 59.5% lower | 150.7% higher |
| HTTP/1.1 (keep-alive) | IO | Scaling | 8.19k req/s | 21.90 ms | 1.50k req/s | 102.60 ms | 445.8% higher | 78.7% lower | 6.23k req/s | 28.20 ms | 31.6% higher | 22.3% lower |
| HTTP/1.1 (keep-alive) | CPU | Fixed | 7.04k req/s | 28.30 ms | 8.62k req/s | 21.50 ms | 18.3% lower | 31.6% higher | 7.07k req/s | 32.10 ms | 0.4% lower | 11.8% lower |
| HTTP/1.1 (keep-alive) | CPU | Scaling | 7.78k req/s | 25.70 ms | 8.62k req/s | 21.50 ms | 9.8% lower | 19.5% higher | 7.07k req/s | 32.10 ms | 9.9% higher | 19.9% lower |
| HTTP/2 | IO | Fixed | 1.50k req/s | 128.92 ms | N/A | N/A | - | - | 6.57k req/s | 27.29 ms | 77.2% lower | 372.4% higher |
| HTTP/2 | IO | Scaling | 9.41k req/s | 19.30 ms | N/A | N/A | - | - | 6.57k req/s | 27.29 ms | 43.1% higher | 29.3% lower |
| HTTP/2 | CPU | Fixed | 7.75k req/s | 26.55 ms | N/A | N/A | - | - | 7.24k req/s | 49.42 ms | 7.1% higher | 46.3% lower |
| HTTP/2 | CPU | Scaling | 7.54k req/s | 26.97 ms | N/A | N/A | - | - | 7.24k req/s | 49.42 ms | 4.2% higher | 45.4% lower |
ruby 4.0.7 (2026-09-15 revision 229531a6cf) +YJIT +PRISM [aarch64-linux] 10 worker processes; fixed Raptor and Puma run 3 threads per worker; scaling Raptor starts at 3 with no fixed limit; Falcon runs unbounded fibers per worker; 120 concurrent HTTP/1.1 client connections; 40 concurrent HTTP/2 client connections × 3 streams each
See bin/benchmark for more details.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake to compile native
extensions and run the tests. You can also run bin/console for an interactive prompt that will allow you to
experiment.
On macOS (or any non-Linux host), bin/dev builds and drops you into a Docker image with Ruby and the required Linux
toolchain preinstalled, mounting the repo at /workspace. Run bin/dev for an interactive shell, or
bin/dev <command> for one-off commands like bin/dev bundle exec rake or bin/dev bin/benchmark.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/joshuay03/raptor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Raptor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.