Kino is simply a high-performance Ractor web server for Ruby 4.0+.
Ruby threads cannot tally Ruby codification successful parallel, truthful accumulation setups fork a process per halfway and salary for each transcript successful memory. Kino runs your code on each halfway successful one mini process. A Rust (tokio + hyper) front-end owns the network, parallel Ractors tally your Rack 3 app, and a threaded fallback mode runs everything else, Rails included.
- Fast. On a existent 8-core server, each Kino mode is 1.5-2× ahead of a Puma fork cluster connected I/O-light endpoints. Ractor mode also wins connected axenic CPU, 30%+. Benchmarks below.
- A fraction of the memory. About ~7× connected the simplistic bench Ractor app, and astir 4× little memory than a Puma cluster serving Rails successful fallback threaded mode.
- Parallel without forking. Ractor mode runs CPU activity more than 5× faster than Kino's ain GVL-bound threaded mode, successful the same small process.
- Production plumbing included. Graceful drain, clang supervision and respawn, bounded queues pinch 503 backpressure, petition timeouts, hardened intake (slowloris and TLS-handshake deadlines, connection and body-size caps), an on_error hook for your correction tracker, TLS (rustls), unrecorded stats, async entree and app logging.
- Tells you why. kino --check lists precisely what blocks your app from ractor mode, uncovering by finding, truthful you do not person to decode Ractor::IsolationError yourself.
- Puma-shaped. The aforesaid workers × threads topology, a familiar config DSL, a kino CLI. If you tin tally Puma, you tin tally Kino.
N.B.: Ractors are officially experimental successful Ruby 4.0, and truthful is this server. The threaded mode is solid. Still, Kino intends to beryllium the champion measurement to research pinch Ractors today—and the champion Ractor server erstwhile they go stable.
- Why
- Benchmarks
- Install
- Usage
- Config record and CLI
- kino --check
- Request timeouts
- Stats
- Logging
- Timer waits
- Rack 3 compliance
- Rails
The GVL allows only 1 Ruby thread to tally astatine a time. To usage each cores, Ruby servers fork processes, and each fork costs a afloat transcript of the app. Ractors do not person this limit: each 1 has its ain lock, truthful one process tin tally Ruby successful parallel. What was missing is simply a server that dispatches requests to them. Ruby 4.0 reworked Ractors (Ractor::Port, shareable_proc, little fastener contention) and made this worthy building.
Why a Ractor server has to beryllium built this way, and which Rust parts make Ractors accelerated here: doc/why-kino.md. The afloat design notes unrecorded successful doc/architecture.md.
Measured connected a existent server: AWS c7a.2xlarge (8-core AMD EPYC 9R14, 16 GB, Amazon Linux 2023). This is simply a realistic app-server size.
These tables tally a mini synthetic Rack app—plaintext, a 10 KB body, a CPU-bound fib, a 5 sclerosis wait—deliberately small, to measurement the server rather than an app. It is Ractor-shareable, truthful Kino runs it successful :ractor mode (and :threaded for comparison). A existent Rails app is simply a different story: it is not Ractor-shareable, truthful it runs only successful Kino's :threaded fallback, pinch its ain numbers—see Rails below. Ruby 4.0.5 pinch YJIT, each server astatine its defaults: Puma forks 8 workers × 3 threads, Kino stays successful 1 process (8 workers; 1 thread each successful ractor modes, 3 successful threaded). Numbers are req/s by wrk (8-second windows, 64 connections, aforesaid host). Methodology: doc/benchmarks.md.
| /plaintext | 229,534 | 250,222 | 182,997 | 216,994 | 118,176 |
| /10k | 178,083 | 189,862 | 151,034 | 160,400 | 106,768 |
| /cpu (fib) | 77,999¹ | 70,885 | 66,100 | 13,429 | 58,006 |
| /io (5 ms) | 1,552 | 1,551 | 5,888 | 4,709 | 4,693 |
| /io_native | 1,570 | 1,571 | 6,274 | 4,695 | 4,691 |
Memory tells 2 different stories depending connected the app, some by PSS (proportional group size; spot note) aft sustained load.
The mini benchmark app (Ractor-shareable, truthful Kino runs it successful :ractor or :threaded). Kino is ~7× lighter successful :ractor mode, ~10× successful :threaded than the Puma cluster — the spread stays ample because a trivial app is almost all backstage per-worker heap, which copy-on-write can't share:
| :ractor (8×1) | 148 MB | 1,068 MB | ~7× |
| :threaded (8×3) | 107 MB³ | 1,068 MB | ~10× |
A existent Rails app (not Ractor-shareable—Kino's :threaded fallback only, below). The spread is ~4×, smaller because Rails' large framework is shared copy-on-write crossed Puma's forks:
| PSS | 92 MB | 389 MB | ~4× |
"+ lanes" is the experimental per-worker-queue dispatcher (lanes true). It posts the fastest plaintext/10k of immoderate configuration here. Details: doc/benchmarks.md.
¹ Stock settings, nary tuning. Ractor mode thumps the fork cluster connected pure CPU by +34% (+22% pinch lanes). Threaded mode shows the GVL ceiling that every single-process Ruby server hits. The aged CPU-tuning look is retired: its threads 1 half is the default now, and its tokio_threads 1 half costs −12% connected existent hardware; see doc/benchmarks.md.
² Wait-bound throughput is slots ÷ wait, and the default columns bring 8 single-thread workers against the cluster's 24 threads. Kino slots are threads, not processes—when your app waits a lot, raise workers. The workers 32 file is that tuning: +25% complete the cluster connected /io (+34% via Kino.sleep) while still up of it connected axenic CPU, each in one mini process. The costs is the CPU-light rows (32 ractors oversubscribe 8 cores); prime the topology your app's hold profile needs. See doc/benchmarks.md.
³ With MALLOC_ARENA_MAX=2 (the modular Ruby deployment setting; Heroku's default). Without it, 24 threads churning 10 KB responses through 1 glibc heap balloon to ~670 MB—an arena-fragmentation footgun, not a leak, and ractor mode sidesteps it. See doc/benchmarks.md.
A communal first thought is to support your existent server and wrap the app in a ractor pool. We measured that excessively (same box; the study is successful the doc):
| /plaintext | 193,826 | 19,480 | 99,776 |
| /cpu (fib) | 68,061 | 17,755 | 48,721 |
| /io (5 ms) | 4,530 | 1,454 | 1,549 |
Rails is not Ractor-shareable today, truthful Kino serves it successful :threaded fallback — 1 GVL-bound process. On the aforesaid container (examples/rails-hello, edge Rails, production, 8×5):
| Kino :threaded (one process) | 2,637 | 92 MB |
| Puma cluster (8 workers) | 12,138 | 389 MB |
The honorable trade-off: Puma's fork cluster uses each 8 cores, truthful it serves ~4.6× the throughput — astatine ~4× the memory. Ractor-mode Rails would close the throughput spread astatine one-process representation cost; the upstream blockers are tracked successful doc/rails-on-ractors.md.
In short: connected the mini synthetic app, ractor mode thumps fork-level CPU parallelism (5.8× Kino's own GVL-bound threaded mode, +34% complete the cluster) successful 1 process, at about 1/7th of the cluster's representation by PSS (~4× connected a existent Rails app). Every Kino mode is 1.5-2.1× up of the cluster connected I/O-light endpoints. The macOS numbers (secondary; everything location hits the loopback ceiling) and the YJIT × Ractors gotcha are successful doc/benchmarks.md.
Reproduce: bench/run.sh [seconds] [concurrency] for the main table, bench/studies.sh for the follow-ups (CPU recipe, topology, scaling, logging, memory).
You request Ruby >= 4.0. Add Kino to your application's bundle:
or put it successful the Gemfile yourself:
Then make a config and serve:
(After a standalone gem install, the kino bid useful without bundle exec.)
No Rust compiler needed: released versions vessel precompiled autochthonal gems for Linux (x86_64/aarch64, glibc and musl) and macOS (arm64). On other platforms the gem compiles astatine instal time; that needs a Rust toolchain, plus clang/libclang connected Linux.
Or embedded, pinch everything spelled out:
- :ractor: workers Ractors × threads Threads each. The app must be Ractor.shareable? (frozen middleware, shareable_proc endpoints). Forcing :ractor pinch an unshareable app raises Kino::UnshareableAppError. A collapsed ractor returns 500 to its in-flight requests correct away, past respawns.
- :threaded: the aforesaid machinery connected workers × threads plain Threads. Runs any Rack app, including Rails, today. Parallel for I/O, serialized by the GVL for CPU.
- :auto (default): :ractor erstwhile the app is shareable, otherwise a informing and :threaded. One caveat: a class utilized arsenic a Rack app always counts arsenic "shareable" (classes are), moreover if calling it touches unshareable state. Force :threaded for those.
Settings tin unrecorded successful a Puma-style Ruby DSL file. Precedence: explicit kwargs and CLI flags > config record > defaults.
The generated sample documents each directive, including the Rails settings and the capacity notes.
When an app cannot tally successful :ractor mode, Kino tin show you why, instead of leaving you pinch a bare Ractor::IsolationError. The cheque changes nothing (it does not frost your objects) and names each blocker: captured variables pinch the spot they were defined, lawsuit variables by path, and the class-level lawsuit adaptable trap that catches class-style apps:
Exit position is 0/1, truthful it useful successful CI. The programmatic shape is Kino::Check.report(app).
request_timeout: seconds (or request_timeout 30 successful kino.rb) limits how agelong the app whitethorn return to nutrient a response. Past the deadline the client gets an contiguous 504 while the handler keeps running; its late consequence is dropped without harm. Off by default. The handler is deliberately not killed, because interrupting arbitrary Ruby mid-flight is unsafe. A stuck handler still occupies its worker slot until it returns, truthful group the deadline supra your slowest morganatic endpoint and watch stats[:timeouts].
Timeouts defender your app; the web intake guards itself. New connections past max_connections (default: astir of ulimit -n) wait in the kernel backlog; petition bodies past max_body_size (default 50 MB, nil delegates to a fronting proxy) get a 413; and fixed deadlines driblet slow-header clients (15 s), stalled TLS handshakes (10 s), and uploads stalled mid-body (30 s). When a worker catches an app aliases transportation error, on_error ->(error, env) { ErrorTracker.capture(error) } is called after the customer sewage its 500—the only spot a locator sees errors raised while the consequence was being written (in :ractor mode, build the handler pinch Ractor.shareable_proc).
Kino fires 4 lifecycle hooks alongside on_error, divided by firing context.
Worker-context hooks tally wrong the worker and are disposable to each workers:
- after_worker_boot { |worker_id| }: runs erstwhile earlier the worker originates serving, pinch its slot id. In :ractor mode it runs wrong the worker ractor and must beryllium Ractor.shareable_proc.
- after_request_complete { |env, status| }: fires wrong the worker aft each successful response. This is the basking path—leave it unset for zero cost. In :ractor mode it must beryllium Ractor.shareable_proc.
Main-context hooks tally connected the main thread, extracurricular workers, and are plain procs:
- after_boot { }: fires erstwhile aft the worker excavation is up. Wire readiness here—sd_notify, a "server ready" metric, and truthful on.
- on_worker_exit { |worker_index, error| }: fires erstwhile a worker exits, pinch its scale and the clang origin (or nil connected a cleanable exit).
after_worker_boot's statement is the worker's slot id, while successful :ractor mode on_worker_exit's statement identifies the exited ractor (0..workers - 1)—a different number space—so don't correlate footwear and exit by that number successful :ractor mode.
A raising hook is logged and ne'er kills a worker.
quarantine_timeout: seconds (or quarantine_timeout 60 successful kino.rb) quarantines a dispatch slot whose petition has tally longer than the deadline and spawns a replacement worker to reconstruct capacity—distinct from request_timeout, which gives the customer a 504 but leaves the slot occupied. quarantine_max (default: the worker count successful :ractor mode, workers × threads successful :threaded) caps the full number of replacement events complete the process lifetime—past it the show stops replacing and the server runs astatine reduced capacity.
The wedged worker is ne'er interrupted aliases force-killed, and its slot stays quarantined for good. In :threaded mode, if the blocked thread eventually returns, it keeps serving requests connected that aforesaid slot—but the slot itself stays flagged quarantined (busy_ms reported arsenic 0) for the rest of the process; successful :ractor mode the wedged ractor (and its supervisor thread) leaks until the process exits, since a wedged ractor cannot be safely interrupted. Monitor quarantine activity via server.stats (top-level quarantined count and per-slot worker_status[].quarantined flag), GET /stats (same), and GET /metrics (kino_quarantined_workers gauge and kino_quarantine_replacements_total counter).
server.stats returns a unrecorded snapshot: the configuration positive counters from the autochthonal furniture (one relaxed atomic per request, nary measurable cost):
From the outside, termination -USR1 <pid> prints the aforesaid snapshot arsenic 1 line (pair it pinch pidfile to find the pid):
For pull-based monitoring, control_bind "127.0.0.1:9293" (or a unix:// path) serves a read-only control plane from the native layer connected its ain thread—it keeps answering moreover while each Ruby worker is engaged aliases stuck, and reports draining done a graceful shutdown:
- GET /stats—the aforesaid snapshot arsenic server.stats, arsenic JSON (plus state and version).
- GET /metrics—Prometheus matter format (kino_requests_served_total, kino_queue_depth, kino_ready, …).
Both /stats and /metrics besides break the counters down per dispatch slot: /stats carries a worker_status array (index, served, in_flight, busy_ms) and /metrics emits kino_worker_*{worker="N"} series, 1 introduction per execution slot (workers × threads)—a crashed worker's slot is ne'er reused, truthful it stays successful the database pinch its counters frozen wherever they stopped, meaning the array (and its worker="N" metric series) grows by 1 crossed each respawn. busy_ms is really agelong the slot's existent petition has been moving (0 erstwhile idle), truthful a azygous slot climbing while the remainder beryllium astatine 0 is your stuck worker.
The /stats consequence and server.stats transportation queue_time (count and summed seconds), and /metrics exposes kino_request_queue_seconds—a Prometheus histogram of queue-wait time, the worker-saturation signal. Counts admitted requests only; a 503 aft queue hold goes to rejected, not queue_time.
- GET /ready—200 erstwhile serving, 503 while booting aliases draining: wire it to your load balancer aliases Kubernetes readiness probe.
- GET /live—200 whenever the process is alive: the liveness probe.
control_token "..." puts /stats and /metrics behind Authorization: Bearer; the probes enactment open.
With 1 log statement per request, Kino::Logger sustained 2.4× the throughput of a shared ::Logger (149k vs 63k req/s connected the benchmark box). There are 2 autochthonal pieces. Both constitute done a lock-free channel to a Rust flusher thread, truthful petition threads ne'er return a log mutex and ne'er make a constitute syscall:
-
Access log (log_requests true): 1 statement per petition to stdout, including the 503s that ne'er scope your app. Recommended in development; inexpensive capable for production. On colour terminals the lines are tinted by position class: 2xx green, 3xx yellow, 4xx maroon, 5xx agleam red:
127.0.0.1 [Tue, 10 Jun 2026 13:39:56 GMT] "GET / HTTP/1.1" 200 0.1ms -
Kino::Logger: a ::Logger complete the aforesaid async sink, for your app's ain logging (Kino::Logger.new("log/production.log"), aliases no argument for stdout). The earthy IO-like instrumentality is Kino::Logger::Device, for integrations that want bytes without ::Logger formatting. The device is stiff and Ractor-shareable, truthful 1 instrumentality serves every worker.
Kino::Logger successful a Rails app: it is simply a existent ::Logger subclass, so it fits anyplace Rails expects a logger:
From a plain Rack app, springiness middleware the logger, aliases hand Rack::CommonLogger the earthy instrumentality (it conscionable calls write):
(If you only want petition lines, for illustration Kino's ain log_requests true. It is free for your Ruby threads, and it besides sees the 503s that never reach Rack.)
Graceful shutdown drains some logs fully. A difficult clang tin suffer the tail of the buffer, and erstwhile you log faster than the disk tin return (over 100k lines/s), the descend drops lines alternatively of blocking petition threads. These trade-offs are measured in doc/benchmarks.md.
Kino.sleep(seconds) is simply a high-resolution slumber connected the OS timepiece with the GVL released. MRI's ain slumber wakes up precocious wrong non-main ractors (details and numbers successful doc/benchmarks.md). Use Kino.sleep for definitive timer waits successful handlers. Ordinary blocking I/O does not request it.
The spec suite runs each trial app nether Rack::Lint complete existent sockets: streaming petition bodies (forward-only rack.input), enumerable and callable (full-duplex stream) consequence bodies, lowercase and multi-value headers, HEAD/204 semantics. Full hijack is near retired connected purpose; it is optional successful Rack 3.
Rails (edge) runs connected Kino coming successful :threaded mode; see examples/rails-hello. Ractor-mode Rails is blocked upstream. The exact blockers, the Ruby::Box findings, and what would unlock it are written up successful doc/rails-on-ractors.md. The example ships a probe book that re-tests against immoderate Rails you bundle.
Thanks to Mat Sadler for magnus.
For ractors, acknowledgment to Koichi Sasada, John Hawthorn, Jean Boussier, Luke Gruber, and different Ruby halfway contributors.
For the Rust web stack, acknowledgment to Sean McArthur for hyper, and to Carl Lerche, Alice Ryhl, and the different Tokio maintainers for the runtime underneath it. Thanks to Joshua Barretto for flume—its channels transportation each petition betwixt the web broadside and the workers.
Claude Code (Fable 5, Opus 4.8).
Bug reports and propulsion requests are invited connected GitHub at https://github.com/yaroslav/kino.
The gem is disposable arsenic unfastened root nether the position of the MIT License.
English (US) ·
Indonesian (ID) ·