{"id":51438584,"url":"https://github.com/yaroslav/kino","last_synced_at":"2026-07-05T09:30:29.572Z","repository":{"id":364038766,"uuid":"1265848260","full_name":"yaroslav/kino","owner":"yaroslav","description":"A high-performance Ractor web server for Ruby 4.0+: Rack 3-based, with a Rust Tokio/Hyper front-end and Ractor-parallel Ruby workers and threaded fallback mode.","archived":false,"fork":false,"pushed_at":"2026-07-04T11:07:01.000Z","size":292,"stargazers_count":167,"open_issues_count":0,"forks_count":3,"subscribers_count":6,"default_branch":"main","last_synced_at":"2026-07-04T12:16:53.494Z","etag":null,"topics":["concurrency","http-server","hyper","parallelism","ractor","ractors","ruby","ruby-on-rails","rust","tokio","tokio-rs","webserver"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yaroslav.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-11T06:06:46.000Z","updated_at":"2026-07-04T11:05:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yaroslav/kino","commit_stats":null,"previous_names":["yaroslav/kino"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/yaroslav/kino","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaroslav%2Fkino","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaroslav%2Fkino/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaroslav%2Fkino/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaroslav%2Fkino/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yaroslav","download_url":"https://codeload.github.com/yaroslav/kino/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yaroslav%2Fkino/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35150062,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-05T02:00:06.290Z","response_time":100,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["concurrency","http-server","hyper","parallelism","ractor","ractors","ruby","ruby-on-rails","rust","tokio","tokio-rs","webserver"],"created_at":"2026-07-05T09:30:28.970Z","updated_at":"2026-07-05T09:30:29.564Z","avatar_url":"https://github.com/yaroslav.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kino\n\n**Kino** is a high-performance **Ractor** web server for Ruby 4.0+.\n\n[![GitHub Release](https://img.shields.io/github/v/release/yaroslav/kino)](https://github.com/yaroslav/kino/releases)\n[![Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/gems/kino)\n\nRuby threads cannot run Ruby code in parallel, so production setups fork\na process per core and pay for each copy in memory. Kino runs your code\non every core in **one small process**. A **Rust** (tokio + hyper)\nfront-end owns the network, parallel **Ractors** run your Rack 3 app,\nand a threaded fallback mode runs everything else, Rails included.\n\n* **Fast.** On a real 8-core server, every Kino mode is **1.5-2×**\n  ahead of a Puma fork cluster on I/O-light endpoints. Ractor mode also\n  wins on pure CPU, **30%+**. [Benchmarks](#benchmarks) below.\n* **A fraction of the memory.** About **~7×** on the simplistic bench \n  Ractor app, and about **4× less memory** than a Puma cluster serving Rails in fallback threaded mode.\n* **Parallel without forking.** Ractor mode runs CPU work **more than\n  5× faster** than Kino's own GVL-bound threaded mode, in the same\n  small process.\n* **Production plumbing included.** Graceful drain, crash supervision\n  and respawn, bounded queues with 503 backpressure, request timeouts,\n  TLS (rustls), live stats, async access and app logging.\n* **Tells you why.** `kino --check` lists exactly what blocks your app\n  from ractor mode, finding by finding, so you do not have to decode\n  `Ractor::IsolationError` yourself.\n* **Puma-shaped.** The same `workers × threads` topology, a familiar\n  config DSL, a `kino` CLI. If you can run Puma, you can run Kino.\n\n**N.B.:** Ractors are officially **experimental** in Ruby 4.0, and so is this server. The threaded mode is solid. Still, Kino aims to be the best way to experiment with Ractors today—and the best Ractor server when they become stable.\n\n---\n\n## Table of Contents\n\n- [Why](#why)\n- [Benchmarks](#benchmarks)\n- [Install](#install)\n- [Usage](#usage)\n- [Config file and CLI](#config-file-and-cli)\n- [`kino --check`](#kino---check)\n- [Request timeouts](#request-timeouts)\n- [Stats](#stats)\n- [Logging](#logging)\n- [Timer waits](#timer-waits)\n- [Rack 3 compliance](#rack-3-compliance)\n- [Rails](#rails)\n\n## Why\n\nThe GVL allows only one Ruby thread to run at a time. To use all cores,\nRuby servers fork processes, and every fork costs a full copy of the\napp. Ractors do not have this limit: each one has its own lock, so one\nprocess can run Ruby in parallel. What was missing is a server that\ndispatches requests to them. Ruby 4.0 reworked Ractors (`Ractor::Port`,\n`shareable_proc`, less lock contention) and made this worth building.\n\nWhy a Ractor server has to be built this way, and which Rust parts make\nRactors fast here: [doc/why-kino.md](doc/why-kino.md). The full design\nnotes live in [doc/architecture.md](doc/architecture.md).\n\n## Benchmarks\n\nMeasured on a real server: AWS **c7a.2xlarge** (8-core AMD EPYC 9R14,\n16 GB, Amazon Linux 2023). This is a realistic app-server size.\n\n**These tables run a tiny synthetic Rack app**—plaintext, a 10 KB body,\na CPU-bound `fib`, a 5 ms wait—deliberately small, to measure the server\nrather than an app. It is Ractor-shareable, so Kino runs it in `:ractor`\nmode (and `:threaded` for comparison). **A real Rails app is a different\nstory:** it is *not* Ractor-shareable, so it runs only in Kino's\n`:threaded` fallback, with its own numbers—see [Rails](#rails) below.\nRuby 4.0.5 with YJIT, every server at its defaults: Puma forks 8 workers ×\n3 threads, Kino stays in one process (8 workers; 1 thread each in ractor\nmodes, 3 in threaded). Numbers are req/s by wrk (8-second windows, 64\nconnections, same host). Methodology:\n[doc/benchmarks.md](doc/benchmarks.md).\n\n| endpoint    | Kino :ractor | + lanes | :ractor, `workers 32`² | Kino :threaded | Puma (cluster) |\n|-------------|-------------:|--------:|-----------------------:|---------------:|---------------:|\n| /plaintext  |      229,534 | **250,222** |         182,997 |        216,994 |        118,176 |\n| /10k        |      178,083 | **189,862** |         151,034 |        160,400 |        106,768 |\n| /cpu (fib)  |   **77,999**¹|  70,885 |          66,100 |         13,429 |         58,006 |\n| /io (5 ms)  |        1,552 |   1,551 |       **5,888** |          4,709 |          4,693 |\n| /io_native  |        1,570 |   1,571 |       **6,274** |          4,695 |          4,691 |\n\nMemory tells two different stories depending on the app, both by **PSS**\n(proportional set size; see note) after sustained load.\n\n**The tiny benchmark app** (Ractor-shareable, so Kino runs it in `:ractor`\nor `:threaded`). Kino is **~7× lighter in :ractor mode, ~10× in :threaded**\nthan the Puma cluster — the gap stays large because a trivial app is almost\nall private per-worker heap, which copy-on-write can't share:\n\n| tiny app, Kino  | Kino (one process) | Puma cluster (8 workers) | ratio |\n|-----------------|-------------------:|-------------------------:|------:|\n| :ractor (8×1)   |         **148 MB** |                 1,068 MB |  ~7×  |\n| :threaded (8×3) |         **107 MB**³|                 1,068 MB | ~10×  |\n\n**A real Rails app** (not Ractor-shareable—Kino's `:threaded` fallback\nonly, [below](#rails)). The gap is **~4×**, smaller because Rails' large\nframework *is* shared copy-on-write across Puma's forks:\n\n| Rails hello-world | Kino :threaded | Puma cluster (8 workers) | ratio |\n|-------------------|---------------:|-------------------------:|------:|\n| **PSS**           |      **92 MB** |               **389 MB** |  ~4×  |\n\n\"+ lanes\" is the experimental per-worker-queue dispatcher (`lanes true`).\nIt posts the fastest plaintext/10k of any configuration here. Details:\n[doc/benchmarks.md](doc/benchmarks.md#lane-dispatch-experimental-lanes-true).\n\n¹ Stock settings, no tuning. Ractor mode beats the fork cluster on pure\nCPU by +34% (+22% with lanes). Threaded mode shows the GVL ceiling that\nevery single-process Ruby server hits. The old CPU-tuning recipe is\nretired: its `threads 1` half **is** the default now, and its\n`tokio_threads 1` half costs −12% on real hardware; see\n[doc/benchmarks.md](doc/benchmarks.md#cpu-bound-tuning).\n\n² Wait-bound throughput is slots ÷ wait, and the default columns bring\n8 single-thread workers against the cluster's 24 threads. Kino slots\nare threads, not processes—when your app waits a lot, raise `workers`.\nThe `workers 32` column is that tuning: **+25% over the cluster on /io\n(+34% via `Kino.sleep`)** while still ahead of it on pure CPU, all in\none small process. The cost is the CPU-light rows (32 ractors\noversubscribe 8 cores); pick the topology your app's wait profile\nneeds. See\n[doc/benchmarks.md](doc/benchmarks.md#why-io-lags-in-ractor-mode-on-linux).\n\n³ With `MALLOC_ARENA_MAX=2` (the standard Ruby deployment setting;\nHeroku's default). Without it, 24 threads churning 10 KB responses\nthrough one glibc heap balloon to ~670 MB—an arena-fragmentation\nfootgun, not a leak, and ractor mode sidesteps it. See\n[doc/benchmarks.md](doc/benchmarks.md#memory-under-load-and-the-glibc-arena-footgun).\n\nA common first idea is to keep your current server and wrap the app in\na ractor pool. We measured that too (same box; the analysis is in the\ndoc):\n\n| endpoint   | Kino :ractor (8×3) | Puma + ractor wrapper | Falcon + ractor wrapper |\n|------------|-------------------:|----------------------:|------------------------:|\n| /plaintext |        **193,826** |                19,480 |                  99,776 |\n| /cpu (fib) |         **68,061** |                17,755 |                  48,721 |\n| /io (5 ms) |          **4,530** |                 1,454 |                   1,549 |\n\n### Rails\n\nRails is not Ractor-shareable today, so Kino serves it in `:threaded`\nfallback — one GVL-bound process. On the same box (`examples/rails-hello`,\nedge Rails, production, 8×5):\n\n| Rails hello-world            |  req/s | memory (PSS) |\n|------------------------------|-------:|-------------:|\n| Kino :threaded (one process) |  2,637 |    **92 MB** |\n| Puma cluster (8 workers)     | 12,138 |       389 MB |\n\nThe honest trade-off: Puma's fork cluster uses all 8 cores, so it serves\n~4.6× the throughput — at ~4× the memory. Ractor-mode Rails would close\nthe throughput gap at one-process memory cost; the upstream blockers are\ntracked in [doc/rails-on-ractors.md](doc/rails-on-ractors.md).\n\nIn short: on the tiny synthetic app, ractor mode beats fork-level CPU parallelism (**5.8×** Kino's\nown GVL-bound threaded mode, +34% over the cluster) in one process, at\nabout 1/7th of the cluster's memory by PSS (~4× on a real Rails app).\nEvery Kino mode is 1.5-2.1× ahead of the cluster on I/O-light endpoints. The macOS numbers\n(secondary; everything there hits the loopback ceiling) and the\nYJIT × Ractors gotcha are in [doc/benchmarks.md](doc/benchmarks.md).\n\nReproduce: `bench/run.sh [seconds] [concurrency]` for the main table,\n`bench/studies.sh` for the follow-ups (CPU recipe, topology, scaling,\nlogging, memory).\n\n## Install\n\nYou need Ruby \u003e= 4.0. Add Kino to your application's bundle:\n\n```sh\nbundle add kino      # or: gem install kino (outside a bundle)\n```\n\nor put it in the `Gemfile` yourself:\n\n```ruby\ngem \"kino\", \"~\u003e 0.1\"\n```\n\nThen generate a config and serve:\n\n```sh\nbundle exec kino --init    # writes kino.rb; every directive documented in place\nbundle exec kino           # picks up config.ru + kino.rb, serves on :9292\n```\n\n(After a standalone `gem install`, the `kino` command works without\n`bundle exec`.)\n\nNo Rust compiler needed: released versions ship precompiled native gems\nfor Linux (x86_64/aarch64, glibc and musl) and macOS (arm64). On other\nplatforms the gem compiles at install time; that needs a Rust toolchain,\nplus clang/libclang on Linux.\n\n## Usage\n\n```ruby\nrequire \"kino\"\n\n# Ractor mode needs a Ractor-shareable app: capture nothing, freeze config.\napp = Ractor.shareable_proc do |env|\n  [200, { \"content-type\" =\u003e \"text/plain\" }, [\"Hello from #{Ractor.current}\"]]\nend\n\nKino::Server.run(app, port: 9292)   # traps INT/TERM; Ctrl-C drains gracefully\n```\n\nOr embedded, with everything spelled out:\n\n```ruby\nserver = Kino::Server.new(app,\n  bind: \"127.0.0.1\",\n  port: 9292,                 # 0 = ephemeral; read back via server.port\n  workers: Etc.nprocessors,   # ractors (parallelism)\n  threads: 1,                 # per worker; ractor default 1, threaded default 3\n  mode: :auto,                # :auto | :ractor | :threaded\n  queue_depth: 1024,          # bounded queue; overflow → 503\n  queue_timeout: 5.0,         # seconds before 503 on a full queue\n  request_timeout: nil,       # seconds before a slow response becomes a 504 (nil = off)\n  shutdown_timeout: 30,       # drain deadline\n  tls: { cert: \"cert.pem\", key: \"key.pem\" },  # file paths or inline PEM\n)\nserver.start\nserver.shutdown               # graceful: drain → deadline → abort stragglers\n```\n\n### Modes\n\n- **`:ractor`**: `workers` Ractors × `threads` Threads each. The app must\n  be `Ractor.shareable?` (frozen middleware, `shareable_proc` endpoints).\n  Forcing `:ractor` with an unshareable app raises\n  `Kino::UnshareableAppError`. A crashed ractor returns 500 to its\n  in-flight requests right away, then respawns.\n- **`:threaded`**: the same machinery on `workers × threads` plain\n  Threads. Runs **any** Rack app, including Rails, today. Parallel for\n  I/O, serialized by the GVL for CPU.\n- **`:auto`** (default): `:ractor` when the app is shareable, otherwise\n  a warning and `:threaded`. One caveat: a *class* used as a Rack app\n  always counts as \"shareable\" (classes are), even if calling it touches\n  unshareable state. Force `:threaded` for those.\n\n## Config file and CLI\n\nSettings can live in a Puma-style Ruby DSL file. Precedence: explicit\nkwargs and CLI flags \u003e config file \u003e defaults.\n\n```ruby\n# kino.rb\nport 9292\nworkers 8\nthreads 1\nmode :ractor\n```\n\n```sh\nkino --init                   # write a fully commented sample kino.rb\nkino                          # config.ru + kino.rb, port 9292\nkino --check                  # explain whether the app can run in :ractor mode\nkino -C config/kino.rb -p 3000 -w 4 -m ractor my_app.ru\n```\n\nThe generated sample documents every directive, including the Rails\nsettings and the performance notes.\n\n## `kino --check`\n\nWhen an app cannot run in `:ractor` mode, Kino can tell you why, instead\nof leaving you with a bare `Ractor::IsolationError`. The check changes\nnothing (it does not freeze your objects) and names each blocker:\ncaptured variables with the place they were defined, instance variables\nby path, and the class-level instance variable trap that catches\nclass-style apps:\n\n```\n$ kino --check\ncheck: app is NOT Ractor-shareable\n  - app (Proc at app.rb:12)—captures `cache` = {} (Hash) (unshareable)\n  - app (HelloApp).@instance—class-level ivar holds #\u003cHelloApp…\u003e—classes\n    pass Ractor.shareable?, but reading this from a worker ractor raises\n    Ractor::IsolationError on the first request\n  hints: freeze config at boot; build endpoints with Ractor.shareable_proc;\n  keep per-worker resources in Ractor.store_if_absent; or run mode :threaded.\n```\n\nExit status is 0/1, so it works in CI. The programmatic form is\n`Kino::Check.report(app)`.\n\n## Request timeouts\n\n`request_timeout: seconds` (or `request_timeout 30` in `kino.rb`) limits\nhow long the app may take to produce a response. Past the deadline the\nclient gets an immediate **504** while the handler keeps running; its\nlate response is dropped without harm. Off by default. The handler is\ndeliberately *not* killed, because interrupting arbitrary Ruby mid-flight\nis unsafe. A stuck handler still occupies its worker slot until it\nreturns, so set the deadline above your slowest legitimate endpoint and\nwatch `stats[:timeouts]`.\n\n## Stats\n\n`server.stats` returns a live snapshot: the configuration plus counters\nfrom the native layer (one relaxed atomic per request, no measurable\ncost):\n\n```ruby\nserver.stats\n# =\u003e {mode: :ractor, lanes: false, workers: 8, threads: 1, batch: 1,\n#     respawns: 0, queued: 0, in_flight: 2, served: 1041, rejected: 0,\n#     timeouts: 0}\n# plus lane_depths: [...] when lane dispatch is on\n```\n\nFrom the outside, `kill -USR1 \u003cpid\u003e` prints the same snapshot as one line\n(pair it with `pidfile` to find the pid):\n\n```\nKino stats: mode=:ractor lanes=false workers=8 threads=1 batch=1 respawns=0 queued=0 in_flight=2 served=1041 rejected=0 timeouts=0\n```\n\n## Logging\n\nWith one log line per request, `Kino::Logger` sustained **2.4× the\nthroughput of a shared `::Logger`** (149k vs 63k req/s on the benchmark\nbox). There are two native pieces. Both write through a lock-free\nchannel to a Rust flusher thread, so request threads never take a log\nmutex and never make a write syscall:\n\n- **Access log** (`log_requests true`): one line per request to stdout,\n  including the 503s that never reach your app. Recommended in\n  development; cheap enough for production. On color terminals the\n  lines are tinted by status class: 2xx green, 3xx yellow, 4xx maroon,\n  5xx bright red:\n\n  ```\n  127.0.0.1 [Tue, 10 Jun 2026 13:39:56 GMT] \"GET / HTTP/1.1\" 200 0.1ms\n  ```\n\n- **`Kino::Logger`**: a `::Logger` over the same async sink, for your\n  app's own logging (`Kino::Logger.new(\"log/production.log\")`, or no\n  argument for stdout). The raw IO-like device is `Kino::Logger::Device`,\n  for integrations that want bytes without `::Logger` formatting. The\n  device is frozen and Ractor-shareable, so one device serves every\n  worker.\n\n`Kino::Logger` in a **Rails** app: it is a real `::Logger` subclass, so\nit fits anywhere Rails expects a logger:\n\n```ruby\n# config/environments/production.rb, simplest forms:\nconfig.logger = Kino::Logger.new                          # stdout\nconfig.logger = Kino::Logger.new(\"log/production.log\")    # file\n# both file and stdout:\nconfig.logger = ActiveSupport::BroadcastLogger.new(\n  Kino::Logger.new(\"log/production.log\"), Kino::Logger.new\n)\n# tagged logging wraps it like any ::Logger:\nconfig.logger = ActiveSupport::TaggedLogging.new(Kino::Logger.new)\n```\n\nFrom a plain **Rack** app, give middleware the logger, or hand\n`Rack::CommonLogger` the raw device (it just calls `write`):\n\n```ruby\n# config.ru\nuse Rack::CommonLogger, Kino::Logger::Device.new   # access-style app log\nrun MyApp\n```\n\n(If you only want request lines, prefer Kino's own `log_requests true`.\nIt is free for your Ruby threads, and it also sees the 503s that never\nreach Rack.)\n\nGraceful shutdown drains both logs fully. A hard crash can lose the tail\nof the buffer, and when you log faster than the disk can take (over 100k\nlines/s), the sink drops lines instead of blocking request threads.\nThese trade-offs are measured in\n[doc/benchmarks.md](doc/benchmarks.md#logging-costs).\n\n## Timer waits\n\n`Kino.sleep(seconds)` is a high-resolution sleep on the OS clock with\nthe GVL released. MRI's own `sleep` wakes up late inside non-main\nractors (details and numbers in [doc/benchmarks.md](doc/benchmarks.md)).\nUse `Kino.sleep` for explicit timer waits in handlers. Ordinary blocking\nI/O does not need it.\n\n## Rack 3 compliance\n\nThe spec suite runs every test app under `Rack::Lint` over real sockets:\nstreaming request bodies (forward-only `rack.input`), enumerable and\ncallable (full-duplex stream) response bodies, lowercase and multi-value\nheaders, HEAD/204 semantics. Full hijack is left out on purpose; it is\noptional in Rack 3.\n\n## Rails\n\nRails (edge) runs on Kino today in `:threaded` mode; see\n`examples/rails-hello`. Ractor-mode Rails is blocked upstream. The exact\nblockers, the `Ruby::Box` findings, and what would unlock it are written\nup in [doc/rails-on-ractors.md](doc/rails-on-ractors.md). The example\nships a probe script that re-tests against whatever Rails you bundle.\n\n## Development\n\n```sh\nbin/setup\nbundle exec rake                       # compile, Rust tests, specs, RBS, lint\nRB_SYS_CARGO_PROFILE=dev bundle exec rake compile   # fast dev rebuilds\n```\n\n## Assisted by\n\nClaude Code (Mythos, Opus).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at\nhttps://github.com/yaroslav/kino.\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaroslav%2Fkino","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyaroslav%2Fkino","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyaroslav%2Fkino/lists"}