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.20.0
[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

Configuration

Raptor accepts configuration via command-line flags, a Ruby config file, or both (CLI flags override config file values). Run bundle exec raptor --help for the full flag list.

The config file is a Ruby file that 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. Settings are nested under connection: (shared across protocols), http1: (HTTP/1.1-specific), and http2: (HTTP/2-specific).

# 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,
  },
  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,
}

threads sets the number of application threads each worker keeps running. By default, Raptor adds temporary threads without a fixed limit when queued work is held up by blocking operations. It does not add threads when waiting for the GVL is the bottleneck, and temporary threads leave after the queue drains. Set max_threads to cap growth, or set it to the same value as threads for a fixed pool.

Set cpu_affinity to true to pin each worker to a distinct CPU when the worker count fits within the process's allowed CPU set. It is off by default because container runtimes commonly expose CPUs that are shared with other containers.

Raptor clears application thread locals after each request by default. Set clean_thread_locals to false to disable it. Set clean_fiber_locals to true to run each request in a fresh Fiber, isolating Fiber-local state as well.

RAPTOR_WORKERS, RAPTOR_THREADS, and RAPTOR_MAX_THREADS can set the corresponding options without a config file. Config files override defaults, environment variables override config files, and command-line options override both. RAPTOR_MAX_THREADS=unlimited leaves adaptive growth uncapped.

before_worker_boot and before_worker_shutdown hooks receive the worker index.

Bindings

Raptor accepts multiple binds: URIs across three schemes.

  • tcp://host:port for TCP. Host can be a specific IP, 0.0.0.0 / [::], or localhost (expanded to both IPv4 and IPv6 loopback addresses).
  • unix:///path/to/socket for a Unix domain socket. Stale sockets left by crashed processes are cleaned up automatically.
  • ssl://host:port?cert=/path/to.crt&key=/path/to.key for TLS. HTTP/1.1 and HTTP/2 are negotiated via ALPN.

Multiple binds can be combined freely.

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 (re-exec master, inheriting listening sockets)

Restarts

  • Phased restart (USR1) replaces workers one at a time, waiting for each new worker to boot before retiring the previous one. The master process keeps running, so existing workers continue serving until they are individually replaced. Use to pick up code changes that don't affect the master's boot path.
  • Hot restart (USR2) re-execs the master process with its original command line, inheriting the listening sockets so accepted connections continue to be served across the swap. The successor master re-runs initialization from scratch. Use to pick up changes that affect master-level state (config layout, dependency upgrades, Raptor itself).

systemd

Raptor implements socket activation (LISTEN_FDS) and sd_notify, so it integrates cleanly with Type=notify units. When the socket unit is active, systemd hands the pre-bound listening file descriptors to Raptor, which serves them in place of binds:. READY=1, STOPPING=1, and RELOADING=1 lifecycle messages are emitted automatically.

# /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

Each worker writes per-worker stats (request count, busy and available threads, backlog, last check-in) to shared memory and to a JSON file (default tmp/raptor.json; set via stats_file).

> 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 expose cluster stats over /stats. For adaptive pools, max_threads in each worker's status is its current thread count, so pool_capacity / max_threads measures the capacity available at that moment rather than comparing against an unbounded configured limit. The control server is read-only and currently exposes only /stats.

(Micro) Benchmarks

Raptor 0.20.0 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 ±21.7% for throughput and ±31.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.81k req/s 82.40 ms 1.51k req/s 124.90 ms 86.5% higher 34.0% lower 11.81k req/s 14.70 ms 76.2% lower 460.5% higher
HTTP/1.1 IO Scaling 7.17k req/s 30.50 ms 1.51k req/s 124.90 ms 375.3% higher 75.6% lower 11.81k req/s 14.70 ms 39.2% lower 107.5% higher
HTTP/1.1 CPU Fixed 7.20k req/s 37.00 ms 8.59k req/s 21.00 ms 16.1% lower 76.2% higher 6.51k req/s 28.10 ms 10.6% higher 31.7% higher
HTTP/1.1 CPU Scaling 6.91k req/s 38.60 ms 8.59k req/s 21.00 ms 19.6% lower 83.8% higher 6.51k req/s 28.10 ms 6.1% higher 37.4% higher
HTTP/1.1 (keep-alive) IO Fixed 2.81k req/s 50.60 ms 1.46k req/s 109.90 ms 92.0% higher 54.0% lower 6.33k req/s 27.60 ms 55.6% lower 83.3% higher
HTTP/1.1 (keep-alive) IO Scaling 7.71k req/s 24.20 ms 1.46k req/s 109.90 ms 426.9% higher 78.0% lower 6.33k req/s 27.60 ms 21.8% higher 12.3% lower
HTTP/1.1 (keep-alive) CPU Fixed 5.90k req/s 33.10 ms 8.27k req/s 22.70 ms 28.7% lower 45.8% higher 6.86k req/s 32.90 ms 14.0% lower 0.6% higher
HTTP/1.1 (keep-alive) CPU Scaling 5.96k req/s 34.90 ms 8.27k req/s 22.70 ms 28.0% lower 53.7% higher 6.86k req/s 32.90 ms 13.2% lower 6.1% higher
HTTP/2 IO Fixed 1.23k req/s 146.17 ms N/A N/A - - 6.57k req/s 27.30 ms 81.3% lower 435.4% higher
HTTP/2 IO Scaling 6.53k req/s 28.69 ms N/A N/A - - 6.57k req/s 27.30 ms 0.6% lower 5.1% higher
HTTP/2 CPU Fixed 6.25k req/s 32.60 ms N/A N/A - - 8.39k req/s 26.63 ms 25.5% lower 22.4% higher
HTTP/2 CPU Scaling 5.64k req/s 32.63 ms N/A N/A - - 8.39k req/s 26.63 ms 32.8% lower 22.5% higher

ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +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.