Igropyr — a fault-tolerant web server in pure Chez Scheme
01 · Fault tolerance
Let It Crash
Every request runs in a supervised worker pool.<br>Handlers don't defend — they crash, and the system recovers.
Crashes heal themselves
A crashed worker is replaced instantly ; the task is<br>retried on a fresh worker, up to 3 times, before the client<br>sees an error.
A worker stuck longer than 30 s — even a CPU-spinning<br>loop — is killed and replaced : preemptive scheduling<br>means nothing can freeze the server.
A half-sent request parks only its own reader process<br>and is reaped by timeout. Other connections never notice.
Write the happy path. The supervisor owns the sad one.
(app-get app "/crash"<br>(lambda (req res)<br>;; the worker dies; the supervisor retries on a<br>;; fresh worker -- the pool refills itself<br>(raise 'handler-crashed)))
(app-get app "/stuck"<br>(lambda (req res)<br>;; a hot loop cannot freeze the system: preemption<br>;; keeps serving, the ticker kills this worker<br>(let loop ((n 0)) (loop (+ n 1)))))
02 · Live systems
Hot code swapping
Replace the handler — or a single route — on a running<br>server. The listener, open connections and the worker pool stay up;<br>in-flight requests finish on the old code.
Deploy without a restart
Routes live in a mutable registry behind the pool. Re-registering<br>a path replaces it atomically for the next request;<br>http-swap! replaces the whole handler the same way.
Combined with graceful shutdown (http-shutdown!<br>drains in-flight work) and SO_REUSEPORT multi-process<br>listening, zero-downtime operation is the default, not a project.
(app-get app "/version"<br>(lambda (req res) (send-text! res "v1")))
;; hit /upgrade on the LIVE server:<br>(app-get app "/upgrade"<br>(lambda (req res)<br>(app-get app "/version" ; re-register =<br>(lambda (req res) ; hot replace<br>(send-text! res "v2 (hot swapped)")))<br>(send-text! res "upgraded")))
03 · The remote retry ring
Faults speak a protocol
When retries are exhausted or a stuck worker is killed,<br>Igropyr doesn't just throw a 500 — it can tell the client exactly<br>what happened, on a connection that stays open.
Killed first, told after
The on-failure hook answers a structured fault<br>after the stuck worker is dead — so when the client hears<br>stuck, there is no execution left in flight .<br>The state is definite.
crash — retries exhausted; resubmit with changed<br>parameters, or compensate.
stuck — killed mid-flight; resubmit carrying state,<br>or roll back.
Keep-alive survives the fault, so the client resubmits on the<br>same connection and gets a fresh retry round. Shorten<br>stuck-ms and a user who once stared at a spinner<br>for 30 s now rings through several informed retries in the<br>same time — failures become invisible at the UI.
(app-listen app 8080<br>`((stuck-ms . 3000) ; fail fast<br>(check-ms . 1000)<br>(on-failure . ,(make-fault-handler))))
;; the client receives, connection kept alive:<br>;; {"fault":"crash","attempts":4,"retryable":true}<br>;; {"fault":"stuck","elapsed-ms":3012,...}<br>;; unset? the plain 500 remains. zero breakage.
04 · Web programming with continuations
Dialogues are processes
A multi-request dialogue — a wizard, a booking, a<br>transfer — runs as one green process . Its local bindings are<br>the conversation state, including things a session store can never<br>hold: an open database transaction , spanning rounds.
Control flow is program text
"The user is at the confirm step" means the process is parked<br>at that line . A step order the code cannot express cannot<br>happen — no state machine to get wrong, no replay to defend<br>against.
The gone guarantee: death for any reason —<br>crash, TTL, completion — unregisters the process, and a later<br>resume answers gone. Dead process = dropped<br>connection = the database itself rolled back:<br>gone proves nothing committed. Together with<br>the fault codes above, the client always knows the definite<br>server state — a complete remote transaction ring.
(conversation-start!<br>(lambda (req suspend!)<br>(let ((tx (begin-tx!))) ; live, across requests<br>(guard (e (#t (rollback! tx) (raise e)))<br>(let ((req2 (suspend! confirm-page)))<br>(commit! tx)<br>done))))<br>req)
(conversation-resume! id req) ; => reply | 'gone<br>;; 'gone means: rolled back. guaranteed.
Foundations
What it stands on
Pure Chez Scheme
Every line is Scheme — R6RS libraries in .sc, no C<br>shim. libuv, zlib and the crypto for MySQL auth are reached<br>through Chez's FFI directly. Whole-program compilation folds the<br>framework and your app into one optimized binary.
Erlang-style actors
Green processes with spawn / send / receive,<br>link and monitor, a process registry,<br>gen-server and PubSub. One OS thread, preemptive<br>scheduling, pure message passing — no shared state, no locks.
Async on libuv
One event loop feeds thousands of parked processes. DNS, file<br>reads and database round-trips park the calling process,<br>never the thread. Non-blocking HTTP/WebSocket clients and<br>Redis/MySQL drivers included.
120k+<br>req/s, keep-alive, laptop
failed requests under ab -c 500
≤35s<br>full...