Repository navigation
Releases: xuckless/anole
Release list
0.4.0
The gRPC/HTTP/3 release: raw TCP and UDP servers, background jobs, webhooks, realtime topics, object storage, mail and AWS Lambda, all on the same server core, the same build flags and the same C ABI.
zig fetch --save git+https://github.com/xuckless/anole#v0.4.0
Added
- Raw TCP and UDP servers:
app.tcpandapp.udprun on Anole's runtime and listeners. TCP comes with framing (lines, length prefixes, varints) and TLS. - Jobs and cron: background jobs with retries, stored in memory, PostgreSQL or Redis, with cron schedules and a JSON admin API.
- Webhooks: outgoing, signed the Standard Webhooks way, with retries; incoming, with signature checks.
- Realtime:
- SSE topics: one publish reaches every subscriber over HTTP/1.1, HTTP/2 and HTTP/3, with replay on reconnect;
watchover PostgreSQL, Redis and MongoDB change streams.
- gRPC over HTTP/3 and Unix sockets, for the server and the client.
- S3-compatible object storage: uploads, downloads and serving.
- SMTP mail: plain, STARTTLS and implicit TLS.
- AWS Lambda: set
deployment = .lambdaand the same app runs behind the Lambda Runtime API. - C ABI 0.10: every area above is reachable from the bindings. A binding built against 0.9 needs a rebuild.
- A new startup banner.
Fixed
- Live console on macOS: no key reached it before this release.
- Live console: a use after free could crash the server at shutdown.
The full list, including the breaking changes, is in CHANGELOG.md.
The anole command line for five platforms is attached below, with SHA256SUMS.
Anole 0.3.1
A small release on top of 0.3.0 with two fixes.
zig fetch --save git+https://github.com/xuckless/anole#v0.3.1
Fixed
- HTTP/3 client: a download could stall after a NAT rebinding moved the client's port while it had nothing left to acknowledge. The server kept sending to the old port and never heard from the new one, until the request timed out (#98).
- The client now sends a keep-alive PING after 5 s without traffic while a stream is open (
ConnectOptions.keep_alive,nullfor none), so the server sees the new address and follows it. - In a 4-CPU Linux container the test went from 5 stalls in about 1,600 runs to 0 in 1,760.
- The client now sends a keep-alive PING after 5 s without traffic while a stream is open (
- Windows (development target): the CI job is green again.
- The console's terminal-side tests are skipped on Windows, since the console isn't there yet; one of them spun forever on the stub clock.
- The console's golden files keep LF line endings.
- An HTTP/2 stream-reset test waits for its handler instead of sleeping 100 ms.
The anole command line for five platforms is attached below, with SHA256SUMS.
Anole 0.3.0
Anole 0.3.0 is the developer-experience release. My goal for it was that starting, configuring, testing and watching an Anole app feels as easy as it does on Express, Fiber or Bun.
zig fetch --save git+https://github.com/xuckless/anole#v0.3.0
What's new
- Settings,
.envfiles and deployment modes.anole.config.load(T, ...)reads typed settings from defaults, files and the environment..envfiles are read in development.environment(development or production) anddeployment(standalone, behind_proxy, anole_lb, platform) set sensible defaults that any explicit option overrides, and the server prints what they changed at startup. - Validation and typed handlers. Field rules on structs, all violations reported at once as a 422
problem+jsonfromreq.bind, andanole.In(.{ .path, .query, .headers, .body })for typed handler inputs. Route paths are checked at compile time. - Logging. JSON Lines logs by default in production, readable logs in development, and startup errors as two readable lines: what failed and how to fix it.
- Testing.
anole.testinggives an in-memory client for routes and middleware. - Resilience. Retries with backoff and jitter, retry budgets, and circuit breakers. PostgreSQL, Redis and MongoDB calls fail fast with 503 while their breaker is open.
- Messaging. A NATS client and my own Kafka client (produce, consumer groups, compression), both also in the C ABI.
- OpenAPI. An OpenAPI 3.1 document generated from typed routes, validator rules and protobuf, for REST, transcoded gRPC and Connect.
- The
anolecommand line.anole newscaffolds a project from templates (minimal, rest-postgres, realtime, grpc, full). Prebuilt binaries for five platforms are attached below, with SHA256SUMS. - A live console.
anole consoleattaches to a running app over a local socket and shows logs, requests, errors, connections, databases, WebSocket and HTTP/3 state, config and routes, and can reload or drain the app. It's on by default and costs nothing while nobody is attached. - Runtime gauges per executor in
/metrics. - C ABI 0.9, with everything above that a Node or Python app would use.
Breaking changes
req.bindvalidates by default: a JSON body whose values don't fit the type is now a 422 with the violations, not a 400.req.bindWithkeeps the old behaviour.- Route methods take their path at compile time. Use
app.addRoute(method, path, handler)for a path known only at run time. - WebSocket over HTTP/2 (RFC 8441) is now off by default. In my measurements it used about twice the CPU of WebSocket over HTTP/1.1 (#91). Turn it on with
http2.enable_connect_protocol = true. timeouts.keepalivedefaults to 75 s, up from 60 s, so it sits above the idle timeout of nginx and AWS load balancers.- The access log's JSON lines follow the shared log record (
timereplacests).
docs/migrating.md has every change with before and after code.
Performance
I compared 0.3.0 with 0.2.1 on my x86 Linux box: three rested rounds per case in rotating order. HTTP/1.1, HTTP/2, HTTP/3, PostgreSQL and WebSocket are all within noise of 0.2.1, and instructions per HTTP/1.1 request are within 0.4%. With the console enabled but nobody attached, HTTP/3 on io_uring is about 1.7% slower; I'll follow that up in #103.
Fixed along the way
- A NATS
connect()with reconnect off could report a failure although the connection had been made, when the server hung up right after the handshake. - Kafka now works with brokers that serve Produce v7 and ListOffsets v4, such as Redpanda 25.2.
Known issues
- On Windows, a development-only target, one test still hangs. I'll fix it in 0.3.1, together with an HTTP/3 client stall after a NAT rebinding mid-download (#98).
The full list is in CHANGELOG.md.
Anole 0.2.1
0.2.1 is the relay pass: I went through every path where one connection sends to another (WebSocket rooms and relays, the C ABI's sends, WebSocket over HTTP/2, HTTP/3 and WebTransport streams) and made them cheaper in CPU and memory. Nothing in the public API changed; the C ABI is 0.8 (one new call, anole_wt_stream_set_priority).
Measured on my x86 Linux box against 0.2.0, Anole only, rested and interleaved runs (raw results in bench/results/20261023-relay-pass-ab and 20261023-relay-pass-rerun on the bench/suite branch):
- WebSocket video-call relay: about 9% more users at good quality, at less CPU per message.
- WebTransport video-call relay: about 8% more users at about half the memory per user, and audio now keeps playing when video collapses (it used to wait behind a stalled keyframe).
- Room fan-out (1,000 sockets at 1,000 Hz): 2.4x the messages delivered at a quarter of the memory.
- HTTP/3: about 35% less memory under load; req/s +2.9% on epoll.
- Writes now return once the data is buffered instead of waiting for the peer's acknowledgements; back-pressure is reported, not hidden inside a send.
Fixed along the way: a WebSocket over HTTP/2 message sent from another connection's task could wait until the peer sent something; large messages over WebSocket on HTTP/2 were held by flow control.
Known trade-offs, each with an issue: under fan-out past saturation, a handler's own reply waits behind the larger send rounds (#92); WebSocket over HTTP/2 costs about twice the CPU per message of HTTP/1.1, so it goes off by default in 0.3.0 (#91); WebSocket memory per user and HTTP/3 on io_uring are slightly behind 0.2.0 (#93, #94).
Install: zig fetch --save git+https://github.com/xuckless/anole#v0.2.1. Full list in CHANGELOG.md.
0.2.0
0.2.0 is the API review I wanted done before the Node and Python bindings build on Anole. It batches every breaking change of the 0.x API in one release, so there's a migration to do; docs/migrating.md has an old -> new table for each area.
zig fetch --save git+https://github.com/xuckless/anole#v0.2.0Breaking, in short
- Options are flat and typed.
ServerOptions.serveris gone: its settings are flattimeouts,limitsandproxygroups named as in the C ABI. Durations areanole.Duration(.fromSeconds(10)), TLS has one shape everywhere (anole.Cert), and every middleware is built withinit(.{ ... }). - Errors are named sets.
init,listen,listenTls,runandwaitReadyreturnanole.ServerError; misusing a request or response (a secondreq.reader(), ...) is aMisuseError, not an assert. - Safer defaults. The graceful drain waits 10 s (was 30 s), WebSocket messages are capped at 1 MiB (was 16 MiB), and database calls are bounded by 30 s unless a call opts out.
- Databases speak one vocabulary across SQLite, PostgreSQL, MongoDB and Redis (
table/execValues, oneOptionsshape). - C ABI 0.7. Request calls share the
anole_req_prefix, cleanup calls are idempotent,ANOLE_EV_ABORTsays why, request records are 128 bytes, and event types and handle kinds have one frozen allocation table. A library only serves code built against the same ABI version before 1.0, so rebuild against the newanole.h.
New
app.listen("3000")listens on every interface;app.rateLimit(.{ .limit = 100 })is a one-line rate limiter.- WebSocket subprotocols and handshake headers, handshakes decided by the application (also in the C ABI), and in-order publishes per topic.
timeouts.write,req.setDeadline,req.readerLimited,res.headersSent(),EventStream.commentfor heartbeats.- C ABI: database transactions (
anole_db_begin), queries tied to the request they serve,anole_server_open,anole_last_error_name(), andinclude/anole.abi.jsonfor binding generators.
Fixed
Among others: event streams and downloads are no longer cut at the request timeout, setting any http2 option no longer drops the stream limit, C ABI executors no longer spin while a consumer is idle, and a server with many thousands of connections on one core accepts new ones promptly again (p99 at 25,000 connections went from about 6 s to about 0.2 s).
Checked
A 2-hour soak at half capacity across HTTP/1.1, TLS, HTTP/2 and HTTP/3: 75.4 million requests, no errors, memory flat (16.5-19.8 MB). A Debug run reports no leaks. Against 0.1.3 on one CPU: HTTP/1.1 and HTTP/2 within ±2%, HTTP/3 on epoll +14% with its p99 cut by more than half.
Full list in CHANGELOG.md.
Anole 0.1.3
A security release. I found these while reviewing the API before 0.2.0. Please upgrade:
zig fetch --save git+https://github.com/xuckless/anole#v0.1.3Security
res.sendFilecould read files outside its directory. (GHSA-wmj5-wvf6-w55f) A..segment or an absolute path in request input (a route or query parameter) opened any file the server could read. It now answers 404, as static routes already did. Static routes were not affected.- WebSocket
Roomscould broadcast to a freed socket. (GHSA-8vq8-rhf6-gfw8) A socket that ended without callingleavestayed in its rooms. A socket now leaves every room when it is released, and a member closed for not keeping up has its connection ended (on Windows too). - C ABI: a handle from a destroyed server could act on the next one in the same process. (GHSA-5xpj-5v44-fx4p) Such handles are now always
ANOLE_E_GONE. - Databases: a call that left a transaction open was treated as done. (GHSA-wf2w-hv3j-7jfm) On SQLite every later write joined that transaction; on PostgreSQL each statement committed on its own. Such a call is now rolled back and fails; a whole transaction in one call still works.
- C ABI: drain or reload from another thread during shutdown could use freed memory. (GHSA-whjh-35m7-w456) These calls now wait for a live server or answer as documented.
Each advisory has the details, who is affected and a workaround.
Breaking (C ABI users only)
- C ABI 0.6:
ANOLE_WT_FINis now 2. It was 1, the same bit asANOLE_BODY_BORROW, so a borrowed WebTransport write ended the stream. Rebuild against the newanole.h.
Fixed
- A conflicting route in the C ABI configuration is now
ANOLE_E_CONFIGfromanole_server_startinstead of a crash of the host process.
Full list in CHANGELOG.md.
Anole 0.1.2
Anole 0.1.2 fixes a performance regression from 0.1.1 and brings the C ABI that the Node and Python bindings will build on.
Upgrade
zig fetch --save git+https://github.com/xuckless/anole#v0.1.2Highlights
- PostgreSQL is fast again. 0.1.1 reset every pooled connection when it went back to the pool, which cost about a fifth of the requests per second on pooled reads (HttpArena's async-db profile: 98.9k req/s). Now a connection is reset only when what it ran may have changed its session, and only with the statements that undo that change. On the same box, async-db is back at 123.5k req/s, the same as with the reset turned off, and the protection stays on by default.
- The server's HTTP client keeps its connections. With many handlers calling one upstream at once, the client's small idle pool closed and redialed connections every round, and the server's memory grew without end. Each executor now has its own pool with sensible limits and an idle timeout.
- The C ABI (
anole.h, ABI 0.5) covers the planned scope: HTTP/1.1, HTTP/2 and HTTP/3 with routing and WebSocket topics (from 0.1.1), and now:- databases (SQLite, PostgreSQL, Redis/Valkey, MongoDB), with coalescing and a TTL result cache;
- a gRPC server whose methods are implemented in the host language;
- WebTransport sessions, streams and datagrams;
- mutual TLS, the client certificate on requests, and certificate reload.
- HTTP/3 interop. Against the QUIC interop runner's 15 client implementations, the server now passes 233 cases (207 in 0.1.0), with fixes in path validation, NAT rebinding, Retry, QUIC v2 and ALPN selection. Three of them are patches to the bundled ngtcp2.
Breaking changes
anole.pg.Options.reset_sessionis now an enum,.autoby default.truebecomes.always(or keep the default) andfalsebecomes.off; in the C ABI's configuration,"reset_session"takes"auto","always"or"off". Use.alwaysif server-side functions change session state, since.autojudges from the SQL text.- HTTP client:
client.poolis nowclient.pool(), the calling executor's pool.
Everything else is listed in CHANGELOG.md.
Anole 0.1.1
Anole 0.1.1 is a security release. Everyone on 0.1.0 should upgrade.
Before building the language bindings on top of Anole, I had the whole codebase reviewed for security. The review found 1 critical, 13 high, 25 medium and 29 low-severity problems, and this release fixes all of them. I reproduced each one with a failing test before fixing it, and added fuzz targets where a parser was involved. Two of the problems were in dependencies, zig-protobuf and pg.zig. I told their maintainers privately before this release. Anole now pins patched forks of both, and the zig-protobuf fix is upstream as Arwalk/zig-protobuf#191.
The 30 advisories are listed on the Security tab. The full list, finding by finding, is in CHANGELOG.md.
Upgrade
zig fetch --save git+https://github.com/xuckless/anole#v0.1.1Some fixes change defaults or refuse input that 0.1.0 accepted. Check the breaking changes below before you deploy.
Breaking changes
Each item is described in the changelog.
- Server:
res.header/res.addHeaderrefuseTransfer-Encodingand
Connection(useres.keepalive = false); HTTP/1.1 requests without
exactly oneHostget 400 (request.require_host = falserestores the
old behaviour);X-Forwarded-Hostneedstrust_forwarded_host = true;
a malformed request body reads aserror.MalformedBody. http3.hq_interop_rootneedspub const anole_hq_interop = true;in the
root file.- TLS 1.2 listeners accept only ECDHE with AES-GCM or ChaCha20-Poly1305.
- HTTP client: URLs with raw spaces, control characters or non-ASCII bytes
in the path or query fail witherror.InvalidUrl; a 307/308 that would
carry the body to another host is not followed.StreamErrorgains
HeadersTooLargeandTooManyInterimResponses. - JSON:
json.ErrorgainsNestingTooDeep. - gRPC: a call receives at most 64 MiB in all on the server
(RegisterOptions.max_received_bytes) and on the client
(Channel.max_received_bytes);Call.addTrailerrefuses invalid
metadata (useaddBinaryTrailerfor raw bytes); transcoded bindings with
abodyanswer 415 to a non-JSONContent-Type. - WebSocket and WebTransport: handshakes from another origin get 403 unless
listed inallowed_origins;Rooms.broadcastreturns the number of
members it queued for. - Sessions: signed session cookies from 0.1.0 are refused (users sign in
again);cookie_optsis nowSessionCookieOpts; custom session stores
should implementStore.VTable.updateandtouch. - Auth: a JWKS
Keyringpolicy needs anaudience(or
allow_any_audience = true); OIDC discovery refuses non-https
endpoints;apikey.generatecan returnerror.EntropyUnavailable;
rate-limit keys of IPv6 clients are their /64. - Logs: access logs leave the query string out unless
log_query = true;
incoming request IDs outside[A-Za-z0-9._:-]are replaced. - Databases: PostgreSQL defaults to
sslmode=verify-fullfor remote servers
and sends clear-text or MD5 passwords only to a verified or local server;
SQLite treatsfile:paths as URIs only withOptions.uri = true. examples/restreads its API key from theAPI_KEYenvironment variable.
Also new in 0.1.1
- libanole, a preview of the C ABI (
include/anole.h,zig build capi): the base for the coming Node and Python bindings. ABI 0.x, so there is no stability promise yet. See docs/c-abi.md. - Detached responses in the HTTP core: an HTTP/2 or HTTP/3 request that arrived whole can be answered later, with no task waiting.
- Dual-stack listeners:
listen(":3000")takes both IPv6 and IPv4 clients. This changes behaviour:":3000"used to mean0.0.0.0. - Result cache:
.keep_emptyandanole.db.lastSource(req). - Windows as a development target: the library, the examples and the tests build and run there. Production stays Linux.
Known issue: a program that links the static libanole.a with dead-stripping (-Wl,-dead_strip on macOS) crashes when it starts a TLS listener. A fix is in progress. The Zig package is not affected.
Anole 0.1.0
The first release of Anole: one Zig server for REST, WebSockets, gRPC and WebTransport over HTTP/1.1, HTTP/2 and HTTP/3, with TLS, auth, database connectors and observability built in. It is built on proven libraries (zio, an HTTP core adapted from dusty, nghttp2, BoringSSL, ngtcp2 and nghttp3), and Zig compiles every C dependency, so there is nothing to install but Zig. Every bundled library sits behind a build flag; what is off is neither downloaded nor compiled.
Install
zig fetch --save git+https://github.com/xuckless/anole#v0.1.0Then make build.zig one call:
_ = @import("anole").addApp(b, .{ .name = "api", .root_source_file = b.path("src/main.zig") });Getting started goes from zig init to a running JSON API, then WebSockets, TLS, gRPC and deploying.
Highlights
Protocols
- HTTP/1.1 with keep-alive and pipelining, a router with path parameters and wildcards, nested groups and middleware, per-phase timeouts and body limits, static files with ranges, ETags and precompressed variants.
- HTTP/2 on nghttp2 1.70.0 (with an RFC 9113 strictness patch), over TLS via ALPN and over cleartext with prior knowledge; h2spec passes every case in strict mode.
- HTTP/3 on ngtcp2 1.25.0 and nghttp3 1.18.0: the same handlers over QUIC, Alt-Svc, opt-in 0-RTT with RFC 8470's replay rules, connection migration, and one UDP socket per core on Linux with connection-ID steering. The QUIC interop runner passes every server case it is given.
- Response compression with zstd, gzip (zlib-ng) and, with
-Dbrotli, Brotli; the PROXY protocol andX-Forwarded-*from trusted proxies.
TLS
- BoringSSL built from source by Zig: TLS 1.3 and 1.2, ALPN, mutual TLS on HTTP/1.1, HTTP/2 and HTTP/3, stateless session tickets, certificate reload without a restart (also on SIGHUP), and an in-memory development certificate.
WebSocket and WebTransport
- WebSocket over HTTP/1.1 (RFC 6455), HTTP/2 (RFC 8441) and HTTP/3 (RFC 9220) behind one upgrade, with permessage-deflate, a rooms helper,
app.wsfor a message loop andapp.wsCallbacksfor callbacks. Autobahn passes every case. - WebTransport over HTTP/3 (draft-02, what current browsers speak), server and client: streams, datagrams, close and drain.
gRPC
- Server and client over HTTP/2, all four call modes, deadlines, metadata, gzip and cancellation; interop-tested against grpc-go both ways.
- gRPC-Web, Buf's Connect protocol, the
grpc.health.v1service, and JSON transcoding fromgoogle.api.httpannotations. - Protobuf code generated inside
zig build(addApp,addProtoGen,addProtoModule), with protoc pinned and fetched for you.
Auth
- JWT verification with JWKS keyrings and a bearer-token middleware, OIDC sign-in as a relying party (Google and Apple presets), sessions in memory, Redis/Valkey or sealed cookies, CSRF tokens, Argon2id passwords, API keys, RBAC, and rate limiting in memory or Redis.
Databases
- PostgreSQL, SQLite, MongoDB and Redis/Valkey connectors with one shape: a pool per core, calls that take the request first, rows as your structs in the request's arena, request deadlines that cancel queries, and errors that map to HTTP statuses.
- Opt-in query coalescing, per-core result caches and an HTTP response cache, all off unless asked for.
Observability
- Prometheus
/metrics, OpenTelemetry tracing (W3Ctraceparent, OTLP/HTTP export), JSON or text access logs with request and trace IDs,/healthzand/readyz; the database connectors report into all three.
Operations
- One pinned zio executor per core with connections that stay on the core that accepted them, a graceful drain with a hard deadline, idle HTTP/1.1 and callback WebSocket connections that park without a coroutine, several listeners per server, and a connection limit that follows the open-file limit.
- Zero-downtime restart on SIGUSR2 (listening sockets, HTTP/3's included, are handed to the new process), systemd socket activation and readiness notification without libsystemd, and an opt-in blocking pool for CPU-heavy calls.
The full list, including the API renames made before this release, is in the CHANGELOG.
Requirements
- Zig 0.17.x (tested on 0.17.0). The build stops with an error on any other minor version.
- Linux or macOS. Windows is not supported yet; use WSL2 or Docker until it is (#47).
Status
Anole is pre-1.0: the API is not stable and may change in any 0.x release (the CHANGELOG lists breaking changes). Known limitations and planned work are tracked in GitHub Issues. Results from HttpArena, an independent benchmark, will follow this release.
Links
- Docs: https://xuckless.github.io/anole/
- API reference: https://xuckless.github.io/anole/api/
- Wiki: https://github.com/xuckless/anole/wiki
- CHANGELOG: https://github.com/xuckless/anole/blob/v0.1.0/CHANGELOG.md
- Issues: https://github.com/xuckless/anole/issues
- Security reports: https://github.com/xuckless/anole/security/advisories/new