serverx
Version, currently 0.2.14 versions
- 0.2.3latestOct 16, 2026
- 0.2.2not indexedOct 16, 2026
- 0.2.1not indexedOct 16, 2026
- main branchOct 3, 2026
github.com/OrelSokolov/serverx.cr
nginx-style HTTP server for Crystal: one buffer per connection, offset-based HTTP/1.1 parser with zero per-request allocations in the transport, master/worker processes via SO_REUSEPORT, stock HTTP::Handler API on top (any Crystal framework plugs in). Timeouts, limits, chunked bodies, streaming uploads, graceful shutdown, TLS. Benchmarked against the stdlib HTTP::Server and nginx (scenario 400/410-serverx of the rails.cr cycle).
Nothing has been indexed for 0.2.1 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
serverx:
github: OrelSokolov/serverx.cr
version: ~> 0.2.1Then run:
shards installshard.yml
No shard.yml has been indexed for 0.2.1. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
Server X
nginx-модель HTTP-сервера для Crystal: один буфер (64 KiB) на соединение,
offset-парсер HTTP/1.1 без per-request аллокаций в транспорте,
master/worker через SO_REUSEPORT, совместимый со стандартным
HTTP::Handler — то есть с любым Crystal-фреймворком (Kemal, Lucky, Grip,
rails.cr / action_pack, ...).
Откуда это
Проект начинался как исследовательская попытка ответить на вопрос: что можно улучшить в стандартной библиотеке Crystal, чтобы держать более сильные цифры на бенчмарках. Триггером послужили слова DHH о том, что Rust способен обслуживать его приложение HEY на Raspberry Pi — захотелось выжать максимум из Crystal любыми доступными способами. Для этого брались топовые решения индустрии: nginx-архитектура (процессы + SO_REUSEPORT, буфер на соединение, state-machine-парсер по оффсетам), zero-copy и zero-alloc подходы, переиспользование объектов вместо аллокаций. Итоги исследования — в RESULTS.md: +15–50% RPS на транспорте, GC-churn ниже в 70–370 раз, паритет с nginx на динамических ответах — и главный вывод: решает не экономия байтов в парсере, а процессная модель (тот же Kemal на 8 fork-воркерах даёт ×4.8 RPS и в 3–7 раз лучший p99, чем один процесс).
У zero-alloc есть и прикладной смысл — защита от DDoS на уровне
сервера: когда на запрос не создаётся мусор, сборщику нечего жать, и
под потоком атакующих запросов GC не уходит в бесконечный цикл
выделений/очисток — сервер продолжает отвечать на честный трафик.
Защиту дополняют лимиты транспорта: max_conns (503), max_body (413),
таймауты чтения/записи и отброс битых TLS-handshake.
Эксперимент доведён до продакшен-состояния: хендлеры, таймауты, лимиты, chunked-тела, стриминг uploads, graceful shutdown, TLS, спеки. Бенчмарки — в RESULTS.md.
Быстрый старт
crystal build --release -Dwithout_mt -o bin/serverx src/serverx_cli.cr
./bin/serverx --impl app --workers 8 --port 4500 # сервер с demo-приложением
curl localhost:4500/ # -> Hello, World!
curl localhost:4500/json # -> {"hello":"world"}
Режимы --impl:
app— продакшен-режим: транспорт + стандартныйHTTP::Handler(по умолчанию небольшое demo-приложение);pooled— чистый транспортный бенчмарк: префиксированный ответHello, World!, ноль аллокаций на запрос (сравнимость с RESULTS.md);stdlib— базлайн: голыйHTTP::Serverstdlib с тем же payload.
Подключение своего приложения
Сервер — это библиотека. Бинарник — только demo/бенчмарк.
Как шарда (shard.yml):
dependencies:
serverx:
github: your-fork/serverX.cr
require "serverx"
handler = MyApp::Router.new # любой HTTP::Handler (Kemal.run, action_pack, ...)
ServerX.run(ServerX::Config.new.tap do |cfg|
cfg.impl = "app" # cfg.host / cfg.port / cfg.workers / cfg.max_body ...
end)
или напрямую, без CLI-конфига:
server = ServerX::AppServer.new(handler, "0.0.0.0", 4500,
read_timeout: 30.seconds, # idle keep-alive + чтение запроса
write_timeout: 30.seconds,
max_body: 16_i64 * 1024 * 1024, # больше -> 413
max_conns: 8192, # лимит соединений на воркер, больше -> 503
backlog: 1024)
server.listen # блокирует; SIGTERM/SIGINT -> graceful drain
Мастер-процессы (fork + SO_REUSEPORT, supervise + restart + graceful
shutdown) — ServerX::Cluster.run (требует сборки -Dwithout_mt).
--workers N в CLI делает ровно это.
TLS
Автономный режим без фронта:
./bin/serverx --impl app --port 443 --tls-cert cert.pem --tls-key key.pem --workers 8
Клиенты с брошенным handshake отбрасываются (счётчик tls_drops в Stats).
Частая альтернатива — терминировать TLS на nginx/LB, serverx за фронтом.
Что transport делает за хендлер
- keep-alive и пайплайнинг (регистронезависимые заголовки);
Content-LengthиTransfer-Encoding: chunkedтела: до буфера — из буфера, больше — стримингом из сокета (uploads любого размера доmax_body); пайплайн-байты после тела сохраняются;Expect: 100-continue— интерим-ответ до чтения тела;- тело хендлера не дочитано — транспорт дренит его сам, соединение живо;
- жёсткие ответы: 400 (битая request line / Content-Length, CL+TE вместе —
антисмугглинг RFC 9112 §6.1), 431 (заголовки > 64 KiB), 413 (тело >
max_body), 501 (не-chunkedTransfer-Encoding), 503 (лимит соединений), 500 (исключение в хендлере + лог в STDERR); - HEAD: заголовки без тела — строго по RFC (stdlib, для сравнения, тело пишет);
- upgrade (websockets):
response.upgradeпередаёт соединение блоку.
Архитектура
- Транспорт (
src/serverx/pooled.cr) — единственный буфер на соединение, offset-парсер (один проход по заголовкам), framing без аллокаций. Быстрый путь (--impl pooled) — префиксированный ответ, GC фактически не работает. - Compat-слой (
src/serverx/app_server.cr) — сборкаHTTP::Requestиз оффсетов, переиспользуемыйHTTP::Server::Responseна keep-alive соединении (Response#reset, как в stdlibRequestProcessor), body-IO поверх буфера + стриминг + chunked-декодер на лету. Словарь HTTP (методы, частые имена заголовков) интернирован — литералы вместо аллокаций; заголовки заполняются вrequest.headersнапрямую, без временного хеша и stdlib-dup;BodyReaderпулится на соединении. ~640 Б мусора на запрос — пол stdlib API (см. RESULTS.md). - Мастер (
src/serverx/cluster.cr) — fork/SO_REUSEPORT, supervise (блокирующийProcess#waitна воркера; рантайм Crystal сам жнёт детей по SIGCHLD, поллингwaitpidне работает), рестарт погибших, TERM → drain → KILL по таймауту.
Запуск тестов и бенчей
crystal spec # 24 интеграционных спека (транспорт, app, TLS, лимиты)
./bench.sh # полный прогон (~12 мин, включает nginx в docker)
Зависимостей нет — только stdlib (socket, openssl, http). nginx-сравнение нуждается в docker.
Ограничения (осознанные)
- HTTP/1.1 only: нет HTTP/2 — для него в любом случае нужен фронт.
- Заголовки должны влезать в буфер 64 KiB (иначе 431 + close).
--workers > 1требует сборки-Dwithout_mt(fork несовместим с MT-режимом Crystal 1.21); MT-режим — сборка без флага +CRYSTAL_WORKERS.Expect+ chunked: 100-continue отправляется до тела любой длины.- При переполнении
max_bodyдля chunked-тел ответ может уйти уже начатым — соединение закрывается.
Лицензия
MIT. Проект родился как сценарий 410-serverx бенчмарк-цикла rails.cr
(Crystal-реализация Rails).
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
0.2.1- Tagged
- Oct 16, 2026
- Commit
48ab59636adc- Indexed
- not yet
Dependents
Repository
github.com/OrelSokolov/serverx.cr
Metadata
- Created
- Oct 4, 2026
- Updated
- Oct 24, 2026
- Synced
- Oct 24, 2026
- Versions
- 4