Experimental - under active development

A TypeScript runtime
built for speed

Built from scratch in Zig as a lightweight alternative to Node.js, Deno, and Bun. One binary, zero dependencies, instant cold starts.

3ms runtime init
1.2MB binary size
4MB memory baseline
80K req/s
terminal
$ zig build -Doptimize=ReleaseFast

$ ./zig-out/bin/zigttp-server -e "function handler(r) { return Response.json({hello:'world'}) }"
listening on http://127.0.0.1:8080

$ curl http://localhost:8080/
{"hello":"world"}

What makes it different

Where Node.js, Deno, and Bun optimize for generality, zigttp optimizes for a single use case: running a TypeScript request handler as fast as possible, then getting out of the way.

Opinionated subset

TypeScript with the footguns removed. No classes, no this, no var, no while loops. Unsupported features fail at parse time with a suggested alternative, not at runtime with a cryptic stack trace.

JSX as a first-class primitive

The parser handles JSX and TSX directly. No Babel, no build step, no transpilation. Write server-rendered HTML with components and get back a string.

Compile-time evaluation

comptime() folds expressions at load time, modeled after Zig's comptime. Hash a version string, uppercase a constant, precompute a config value - all before the handler runs.

Native modules over polyfills

Common FaaS needs - JWT auth, JSON Schema validation, caching, crypto, SQL - are implemented in Zig and exposed as zigttp:* virtual modules with zero interpretation overhead.

OpenAPI from type inference

-Dopenapi generates an OpenAPI 3.0 spec directly from handler type analysis. Response schemas, path parameters, and effect metadata - no annotations needed.

Handler effect classification

Each virtual module call carries a proven effect annotation. The compiler aggregates them into handler properties: read_only, retry_safe, deterministic - all in the contract.

Clean, predictable code

Functions in, responses out. No middleware chains, no decorator magic, no hidden state.

function handler(request) {
    if (request.url === "/api/health") {
        return Response.json({
            status: "ok",
            runtime: "zts",
            timestamp: Date.now(),
        });
    }

    return Response.json(
        { error: "Not Found" },
        { status: 404 }
    );
}
function HomePage() {
    return (
        <html>
            <head>
                <title>Hello World</title>
            </head>
            <body>
                <h1>Hello World</h1>
                <p>Welcome to zigttp!</p>
            </body>
        </html>
    );
}

function handler(request) {
    if (request.url === "/") {
        return Response.html(renderToString(<HomePage />));
    }
    return Response.text("Not Found", { status: 404 });
}
import { routerMatch } from "zigttp:router";

const routes = {
    "GET /":            getHome,
    "GET /health":      getHealth,
    "GET /users/:id":   getUser,
    "POST /echo":       postEcho,
};

function handler(req) {
    const match = routerMatch(routes, req);
    if (match) {
        req.params = match.params;
        return match.handler(req);
    }
    return Response.json(
        { error: "Not Found" },
        { status: 404 }
    );
}
import { parseBearer, jwtVerify } from "zigttp:auth";
import { schemaCompile, validateJson } from "zigttp:validate";

schemaCompile("user", JSON.stringify({
    type: "object",
    required: ["name", "email"],
    properties: {
        name:  { type: "string", minLength: 1 },
        email: { type: "string", minLength: 5 },
    }
}));

function handler(req) {
    const token = parseBearer(req.headers["authorization"]);
    if (!token) return Response.json(
        { error: "unauthorized" }, { status: 401 }
    );

    const auth = jwtVerify(token, "my-secret");
    if (!auth.ok) return Response.json(
        { error: auth.error }, { status: 401 }
    );

    if (req.method === "POST") {
        const result = validateJson("user", req.body);
        if (!result.ok) return Response.json(
            { errors: result.errors }, { status: 400 }
        );
        return Response.json({ user: result.value }, { status: 201 });
    }

    return Response.json({ user: auth.value });
}
function TodoForm() {
    return (
        <form
            hx-post="/todos"
            hx-target="#todo-list"
            hx-swap="beforeend">
            <input type="text" name="text" required />
            <button type="submit">Add Todo</button>
        </form>
    );
}

function handler(request) {
    if (request.url === "/" && request.method === "GET") {
        return Response.html(
            renderToString(<TodoForm />)
        );
    }

    if (request.url === "/todos" && request.method === "POST") {
        const html = renderToString(
            <div class="todo-item">New todo</div>
        );
        return Response.html(html);
    }

    return Response.text("Not Found", { status: 404 });
}

Performance

Powered by zts - a pure Zig JavaScript engine with a JIT compiler, NaN-boxed values, and hidden classes. It skips everything a FaaS handler doesn't need.

JS Engine Performance (vs QuickJS)

String Operations
63x faster
16.3M ops/s
Object Create
4.8x
8.1M ops/s
Property Access
3.9x
13.2M ops/s
HTTP Handler
3.1x
1.0M ops/s
Function Calls
2.4x
12.4M ops/s
String Concat
1.3x
8.3M ops/s
Array Operations
1.3x
8.7M ops/s

Cold Start Comparison: zigttp vs Node.js vs Deno vs Bun

zigttp ~71ms embedded bytecode
zigttp ~83ms runtime parsing
Bun ~100-130ms JavaScriptCore
Deno ~150-200ms V8 snapshot
Node.js ~200-300ms V8 + module loading

Runtime initialization is only 3ms. macOS dynamic linker accounts for 80-90ms. Linux static builds target 18-33ms total cold start.

Two-layer architecture

zigttp is a server. zts is the engine. Both are written in Zig. Together they form a purpose-built runtime for handling HTTP requests with JavaScript.

zigttp server

  • HTTP/1.1 listener and parser
  • HandlerPool with lock-free slot acquisition
  • Static file serving with LRU cache
  • CLI with inline eval, CORS, memory limits
  • Build-time handler precompilation
  • Native outbound HTTP bridge

zts engine

  • Two-pass compiler (parse to IR, then bytecode)
  • Stack-based VM with baseline JIT (x86-64, ARM64)
  • NaN-boxed values with hidden classes
  • Polymorphic inline cache (8-entry PIC)
  • Generational GC with hybrid arena allocation
  • Native TypeScript/TSX stripping
1 HTTP request arrives
2 Pool acquires isolated runtime
3 Handler invoked with Request
4 Response sent, runtime released

Virtual modules

Import native Zig implementations with zero JS interpretation overhead. Common FaaS functionality, none of the npm baggage.

zigttp:auth

JWT signing and verification (HS256), bearer token parsing, webhook signature verification, timing-safe comparison.

parseBearer, jwtVerify, jwtSign, verifyWebhookSignature
zigttp:validate

JSON Schema validation with compile-once-validate-many pattern. Supports type, required, properties, min/max, enum.

schemaCompile, validateJson, validateObject, coerceJson
zigttp:cache

In-memory key-value store with namespace isolation, TTL expiry, LRU eviction. Persists across requests in the same pool slot.

cacheGet, cacheSet, cacheDelete, cacheIncr, cacheStats
zigttp:crypto

SHA-256 hashing, HMAC-SHA256, base64 encoding and decoding. All implemented in Zig with no OpenSSL dependency.

sha256, hmacSha256, base64Encode, base64Decode
zigttp:router

Pattern-matching HTTP router with path parameter extraction. Declare routes as an object, match with a single call.

routerMatch
zigttp:env

Environment variable access for configuration. Simple key-value interface for deployment secrets and config.

env
zigttp:sql

SQLite query execution with build-time schema validation via -Dsql-schema. Named parameters, contract-tracked queries.

sql, sqlOne, sqlMany, sqlExec
zigttp:compose

Guard-based handler composition with the pipe operator (|>). Desugared to flat sequential checks at compile time.

guard
zigttp:io

Structured concurrent I/O without async/await. Overlaps multiple fetchSync calls on OS threads (max 8 concurrent).

parallel, race
zigttp:durable

Write-ahead oplog for crash recovery. Idempotent run/step API with timer and signal-based workflow suspension.

run, step, sleep, sleepUntil, waitSignal

zigttp vs Node.js vs Deno vs Bun

How does zigttp compare to the major TypeScript and JavaScript runtimes? Each makes different trade-offs. zigttp sacrifices generality for verification and speed.

zigttp Node.js Deno Bun
Written in Zig C++ Rust Zig + C++
JS engine zts (custom) V8 V8 JavaScriptCore
TypeScript Native stripping Via transpiler Native stripping Native stripping
Cold start ~71ms ~200-300ms ~150-200ms ~100-130ms
Binary size 1.2MB ~80MB ~130MB ~90MB
Memory baseline 4MB ~30MB ~25MB ~20MB
Compile-time verification Yes No No No
Auto sandboxing Yes No Permissions No
Deterministic replay Yes No No No
Dependencies Zero npm npm / jsr npm
Use case Serverless / FaaS General purpose General purpose General purpose

Get started

Requires Zig 0.16.0 or later.

1 Build

$ git clone https://github.com/srdjan/zigttp
$ cd zigttp
$ zig build -Doptimize=ReleaseFast

2 Run

# Inline handler
$ ./zig-out/bin/zigttp-server -e "function handler(r) { return Response.json({ok:true}) }"

# Or with a file
$ ./zig-out/bin/zigttp-server examples/handler.jsx

3 Deploy

# Production build with embedded handler
$ zig build -Doptimize=ReleaseFast -Dhandler=handler.js

# Container: scratch base, 1.2MB total
$ docker build -t myapp .

The language subset

zts implements ES5 with select ES6+ extensions. Every unsupported feature is detected at parse time with a helpful error message suggesting an alternative.

Supported

  • Strict mode, let/const
  • Arrow functions
  • Template literals
  • Destructuring, spread
  • for...of (arrays)
  • Optional chaining, nullish coalescing
  • Pipe operator (|>)
  • Array methods (map, filter, reduce, find)
  • Object.keys/values/entries
  • Typed arrays
  • JSX/TSX (native parsing)
  • TypeScript type stripping
  • comptime() evaluation
  • range(start, end, step)

Intentionally excluded

  • class - use functions and objects
  • this / new - use closures
  • var - use let/const
  • while - use for...of with range()
  • try/catch - use Result types
  • async/await - sequential by design
  • Promises - one request at a time
  • Regular expressions
  • Event loop
  • require / dynamic import

Frequently asked questions

What is zigttp?

zigttp is a JavaScript runtime built from scratch in Zig, designed specifically for serverless and FaaS workloads. It compiles your handler into a single binary with zero dependencies, achieving 3ms cold starts, a 1.2MB binary size, and 80K requests per second throughput.

How is zigttp different from Node.js, Deno, and Bun?

Node.js, Deno, and Bun are general-purpose JavaScript runtimes that support the full language specification. zigttp takes the opposite approach: it implements an opinionated TypeScript subset that removes footgun features (classes, this, async/await, exceptions) at parse time. This constraint enables compile-time verification of handler correctness, automatic least-privilege sandboxing, and deterministic replay - none of which are possible when the runtime must support arbitrary JavaScript. If you need a general-purpose runtime, use Node.js, Deno, or Bun. If you want provably correct serverless handlers with minimal cold starts, zigttp is the better fit.

What JavaScript features does zigttp support?

zigttp supports arrow functions, const/let, destructuring, template literals, for...of, optional chaining, nullish coalescing, the pipe operator, array methods (map, filter, reduce), JSX/TSX, TypeScript type stripping, and comptime() evaluation. Features like classes, var, while loops, async/await, and try/catch are intentionally excluded with parse-time errors that suggest alternatives.

How fast is zigttp compared to Node.js, Deno, and Bun?

zigttp cold starts at ~71ms (embedded bytecode) vs ~100-130ms for Bun, ~150-200ms for Deno, and ~200-300ms for Node.js. The binary is 1.2MB vs 80-130MB for the others. Memory baseline is 4MB vs 20-30MB. Runtime initialization alone is 3ms. These gains come from Zig's zero-overhead abstractions and a custom JS engine (zts) purpose-built for short-lived request handlers.

Is zigttp free and open source?

Yes. zigttp is MIT licensed and available on GitHub. The entire runtime, compiler, and standard library of virtual modules are open source.

What are virtual modules?

Virtual modules (zigttp:auth, zigttp:cache, zigttp:sql, zigttp:io, etc.) are built-in capabilities implemented in Zig with zero interpretation overhead. They provide JWT authentication, key-value caching, SQLite queries, parallel I/O, durable execution, and more - all accessible through standard import syntax and tracked by the compiler for automatic sandboxing.

Can I use zigttp instead of Bun for serverless functions?

Yes, if your use case is request-response serverless handlers. Bun is a general-purpose runtime that supports the full Node.js API, npm packages, and arbitrary JavaScript. zigttp is purpose-built for FaaS: it trades generality for a 1.2MB binary, 4MB memory footprint, compile-time handler verification, and automatic sandboxing. For serverless workloads where cold start time and resource efficiency matter, zigttp is a strong Bun alternative.

Why is zigttp built in Zig instead of Rust or C++?

Zig provides manual memory control with comptime metaprogramming, no hidden allocations, and no runtime overhead - ideal for building a JavaScript engine where every microsecond of cold start matters. Unlike C++, Zig has no hidden constructors or exceptions. Unlike Rust, Zig avoids borrow checker complexity while still preventing undefined behavior through explicit error handling. Zig's comptime evaluation also powers zigttp's build-time handler verification.

Does zigttp support TypeScript?

Yes. zigttp natively strips TypeScript type annotations at parse time with zero overhead - no transpilation step, no tsc, no build tool. You write TypeScript (or TSX for JSX components) and zigttp handles it directly. The supported subset includes const/let type annotations, function parameter types, return types, interfaces, and generics for type checking.

Can I migrate from Node.js or Express to zigttp?

zigttp is not a drop-in replacement for Node.js. It implements a restricted TypeScript subset and does not support npm packages, the Node.js API, or middleware frameworks like Express. However, if your serverless handlers are stateless request-response functions, the migration path is straightforward: rewrite handlers using zigttp's functional style and virtual modules. The result is a provably correct, single-binary deployment with dramatically lower cold starts.