Serverless-first. Built for cold starts, tight memory, and one binary.Deploy guide →

Introduction

Fairyfly is a lightweight backend JavaScript runtime built with Zig and QuickJS. It runs on a single OS thread with an event loop powered by libxev (io_uring on Linux by default, epoll fallback, kqueue on macOS).

Fairyfly is designed first for serverless and edge-style workloads: short-lived processes, fast cold starts, tight memory budgets, and one binary that exits when the event loop drains. It is a native POSIX binary, so it fits container-based serverless platforms and edge VMs rather than V8-isolate runtimes.

165k req/sec · ~10ms guest cold start · 5M reqs at 500 concurrency on 2 vCPU / 1GB microVM

When to use Fairyfly

  • Cold start matters (CLI tools, edge functions, short-lived jobs)
  • Memory is capped (strict container limits)
  • You want the whole runtime to fit in your head (~24k lines of Zig across 54 files)

When NOT to use Fairyfly

  • Browser parity — no DOM, no window object
  • npm ecosystem — no automatic node_modules resolution

If you need those, use Node.js, Bun, or Deno instead.

What's inside

Everything ships in a single binary. No npm install needed for core features.

FeatureDetails
HTTP/HTTPS Server512 connection slots, SoA layout, HTTP/2 via nghttp2
TLSBearSSL, zero-alloc hot path, TLS 1.2 only
SQLiteEmbedded database, single-file amalgamation
WebSocketServer and client, text and binary frames
WorkersUp to 8 threads, cooperative concurrency
FetchOutbound HTTP client with async support
CryptorandomUUID, getRandomValues, subtle.digest
File SystemSync read, write, mkdir, rm, readdir (+ *Async variants)
TimerssetTimeout, setInterval, queueMicrotask
URLFull WHATWG URL API

Three rules

Rule 1: ESM only, extensions optional

Imports are ESM (there is no require). A relative specifier may omit the .js extension — the loader then probes X.js, then X/index.js:

// math.js
export function add(a, b) {
  return a + b;
}

// main.js
import { add } from "./math.js";   // "./math" works too
console.log(add(2, 3)); // 5

Non-.js extensions (.mjs, …) are not probed — write those out.

Bare specifiers like import "lodash" resolve by walking up node_modules/ directories, but only pure-JS ESM packages work.

Rule 2: No CommonJS, no require()

There is no require() function. No module.exports. No __dirname or __filename globals.

Use import/export and import.meta.url instead.

// Wrong — these do not exist
const fs = require("fs");
const __dirname = path.dirname(__filename);

// Right
import { readFile } from "./utils.js";
const dir = new URL(".", import.meta.url).pathname;

Rule 3: No process.nextTick

Use queueMicrotask() instead. It runs before the next timer or I/O event, just like process.nextTick in Node.js.

queueMicrotask(() => console.log("runs next"));
console.log("runs first");

Output:

runs first
runs next