Event loop
Every Fairyfly script runs on one OS thread. Concurrency does not come from threads — it comes from the loop: when your synchronous JS finishes, the runtime dispatches pending timers, I/O completions, and microtasks, and repeats until there is nothing left to do.
The loop is built on libxev: kqueue on macOS, io_uring or epoll on Linux (see Installation).
One turn of the loop
From src/event/loop.zig (runWithMicrotasks), in order:
- Block for completions if referenced work is armed (a timer, socket,
fetch, query…). If only queued submissions remain, drain them without
blocking — that's what lets an
unref()'d timer stop holding the process. - Drain subsystems in a fixed order: fetch → WebSocket client → FFI →
async
fs→ workers. Each drain settles Promises on the JS thread. - Pump microtasks — every
queueMicrotaskcallback queued this turn. - GC check every 8192 turns, and only when the heap exceeds 8 MB.
- Exit or sleep: if nothing relevant is pending, the loop stops and the process exits. If it's idle, it sleeps 100 µs doubling up to 8 ms.
Ordering guarantees
Timers and microtasks are ordered, and you can rely on it:
Rules that fall out of this:
- All synchronous code runs first — always.
- Microtasks run before the next timer or I/O callback.
setTimeout(fn, 0)runs on the next loop turn, which makes it the equivalent of Node'ssetImmediate.- Negative delays are clamped to
0; they defer, they never drop.
What keeps the process alive
The process exits when none of these are true:
An unref()'d timer does not hold the process:
That is the whole pattern for background housekeeping (polling, flushing)
in a CLI: arm it, unref() it, and let the script end.
Async handlers park connections
Inside http.serve, an async handler is suspended — not blocking the
thread — until its Promise settles:
While the Promise is pending, other connections keep flowing. If it never
settles, the watchdog reaps the request with 504 after 30 s. A thrown
error or rejected Promise becomes a 500.
When the process exits
Details and more examples: Errors and exit codes.
Gotchas
- There is no
process.nextTick— usequeueMicrotask. - Sync
fscalls block this thread. In a server, preferfs.*Asyncso a slow disk never stalls connections — but note the Known issue above: a successful*Asynccall still pins the process afterwards. - An infinite JS loop hangs the process — there is no watchdog that can interrupt running JavaScript. See Limitations.
consolewrites to stderr, soff app.js > out.txtcaptures nothing.- The loop wakes at most every 100 µs while idle; don't expect sub-100 µs timer precision.
See also
- Concurrency — loop vs Workers vs the I/O pool
- Timers —
unref,refresh, limits - Errors and exit codes
- Limitations
