Igropyr: Erlang-Style Actor Web Server in Pure Chez Scheme

Igropyr is a web server written entirely in Chez Scheme, implementing Erlang-style actors, fault tolerance via supervisors, hot code swapping, and conversation continuations for multi-request dialogues. It achieves ~35k req/s on an Apple Silicon laptop with 500 concurrent connections (ab -n 50000 -c 500, zero failed requests).

Fault Tolerance: Let It Crash

Igropyr runs every request in a supervised worker pool. Handlers don't defend against errors—they crash, and the system recovers. A crashed worker is replaced instantly; the task retries up to 3 times before the client sees an error. Workers stuck longer than 30 seconds (even CPU-spinning loops) are killed and replaced, thanks to preemptive scheduling. A half-sent request parks only its own reader process and is reaped by timeout—other connections never notice.

(app-get app "/crash"
  (lambda (req res)
    (raise 'handler-crashed)))

Hot Code Swapping

Routes live in a mutable registry behind the pool. Re-registering a path replaces it atomically for the next request. http-swap! replaces the whole handler the same way. Combined with graceful shutdown (http-shutdown! drains in-flight work) and SO_REUSEPORT multi-process listening, zero-downtime operation is the default.

(app-get app "/version"
  (lambda (req res) (send-text! res "v1")))
;; hit /upgrade on the LIVE server:
(app-get app "/upgrade"
  (lambda (req res)
    (app-get app "/version"
      (lambda (req res)
        (send-text! res "v2 (hot swapped)")))
    (send-text! res "upgraded")))

Remote Retry Ring: Faults Speak a Protocol

When retries are exhausted or a stuck worker is killed, Igropyr can tell the client exactly what happened on the same keep-alive connection. The on-failure hook answers a structured JSON fault after the stuck worker is dead—so when the client hears stuck, no execution remains in flight. The state is definite. Keep-alive survives the fault, so the client resubmits on the same connection and gets a fresh retry round.

(app-listen app 8080
  `((stuck-ms . 3000)
    (check-ms . 1000)
    (on-failure . ,(make-fault-handler))))
;; client receives:
;;   {"fault":"crash","attempts":4,"retryable":true}
;;   {"fault":"stuck","elapsed-ms":3012,...}

Conversations: Web Programming with Continuations

A multi-request dialogue (wizard, booking, transfer) runs as one green process. Its local bindings are the conversation state, including an open database transaction spanning rounds. "The user is at the confirm step" means the process is parked at that line. Death for any reason (crash, TTL, completion) unregisters the process, and a later resume answers gone—proving nothing committed.

(conversation-start!
  (lambda (req suspend!)
    (let ((tx (begin-tx!)))
      (guard (e (#t (rollback! tx) (raise e)))
        (let ((req2 (suspend! confirm-page)))
          (commit! tx)
          done))))
  req)
(conversation-resume! id req)   ; => reply | 'gone

Foundations: Pure Scheme, Actors, Async on libuv

Igropyr is built on Chez Scheme—the fastest Scheme compiler—with an FFI that reaches libuv directly. It implements Erlang-style actors (spawn/send/receive, link/monitor, process registry, gen-server, PubSub) on one OS thread with preemptive scheduling. The event loop feeds thousands of parked processes; DNS, file reads, and database round-trips park the calling process, never the thread. Non-blocking HTTP/WebSocket clients and Redis/MySQL drivers are included.

Performance and Included Components

  • ~35k req/s at 500 concurrent connections on an Apple Silicon laptop (ab -n 50000 -c 500, zero failed)
  • 0 failed requests under ab -c 500
  • ≤35s full recovery from a stuck pool
  • Core/framework split like Node and Express: core exposes (http-listen port (lambda (req res) ...)); optional (igropyr express) layer provides create-app, app-get, send-json!, etc.
  • Included: green processes, pure message passing, fault tolerance, failure hook, conversations, hot code swapping, WebSocket (RFC 6455), streaming/SSE, OTP building blocks, JSON parser, S-expression RPC, forms/cookies, middleware suite, chunked transfer-encoding, non-blocking Redis/MySQL clients, async HTTP/WS clients, async file reads, gzip compression, rate limiting, Prometheus /metrics endpoint, runtime introspection, graceful shutdown, multi-process scaling with SO_REUSEPORT, HTTP/1.1 keep-alive and pipelining.

What This Means for Developers

Igropyr brings Erlang/OTP's fault tolerance and hot swapping to the Scheme ecosystem, with a familiar Node-like API. It's ideal for systems requiring high reliability and zero-downtime updates. The conversation model simplifies stateful multi-request interactions without external session stores. If you're comfortable with Scheme and need a robust web server, Igropyr is worth a look.

Getting Started

# macOS: brew install chezscheme libuv
# Debian: apt install chezscheme libuv1-dev zlib1g-dev
git clone https://github.com/guenchi/Igropyr igropyr
export CHEZSCHEMELIBDIRS=.
scheme --script igropyr/test/run-otp.sc
# curl localhost:8080/ — and try killing it: curl localhost:8080/crash