# Xo 0.36 release notes


**Downloads:** [Xo 0.36.0 on GitHub](https://github.com/xo-lang/xo/releases/tag/v0.36.0),
with archives for macOS (Apple silicon and Intel), Linux (arm64 and x86-64), and Windows (x86-64, Authenticode signed), and a `SHA256SUMS` file.

Xo 0.36.0 makes the HTTP stack complete for production services: mutual TLS, HTTP/2 without TLS, trailers, full duplex streams, deadlines on every connection type, and idle keep-alive connections that hold no fiber stack (40,000 idle HTTP/1.1 connections in 255 MB). Programs can also build as whole `wasi:http` components that `wasmtime serve` runs as they are. The compiler's own code is less than half its size at the start of the cycle (71.4 to 32.8 MB of arm64 code), and valued enums arrive in the language.

Four changes break source: std/uuid's `Uuid` fields are private, `Net`, `Proc` and `Fs` gain methods a user implementation must add, a `use go` program that skipped a caller frame for 0.35.0 drops the skip, and Go's in-place `sort` functions are no longer bound (the fourth added on 2026-11-04).

## Breaking changes

- **std/uuid's `Uuid` fields are private** (XO0605; decision 0440).
- **`Net`, `Proc` and `Fs` gain `interfaces()`, `usage()` and `children_usage()`, and `disk_usage()`** (decision 0426): a type implementing one adds them.
- **`use go`: a Go library that records its caller now sees the Xo line**, so a program that added `zap.add_caller_skip(1)` for 0.35.0 drops it. Bindings are rebound once (format 34).
- **`use go "sort"`: `sort.slice`, `slice_stable`, `ints`, `strings` and `float64s` are no longer bound** (a call is `XO1102`): they sort their argument in place, and an Xo List passed to Go is a copy, so the List stayed unsorted with no error. Use `List.sort`, `sort_by` or `sort_in_place`. *(Added 2026-11-04: this change was in the CHANGELOG but missing from these notes when 0.36.0 shipped.)*

## HTTP for production services

- **Mutual TLS** on TLS 1.3 and 1.2, server and client (decision 0433).
- **HTTP/2 without TLS** (h2c) and `H2Config` (decision 0434); **trailers** on HTTP/1.1 and HTTP/2; **full duplex** with `http.pipe()` (decision 0444); `http.Status` and its 62 constants.
- **Idle connections park without a fiber stack** (decision 0449), plain, TLS and HTTP/2: 40,000 idle HTTP/1.1 connections in 293 mappings and 255 MB, 40,000 idle TLS connections in 752,584 kB.
- **Deadlines** on `Conn`, `ServerConn`, `TlsConn` and `WsConn` (decision 0430), and `task.deadline`; WebSocket `on_ping`, `on_pong` and `pong`.
- **A dial races IPv6 and IPv4** as Go's does (decision 0462): on Windows a dial to `localhost` with a server on 127.0.0.1 only no longer waits 2 s for `::1`.

## WebAssembly components

- **`http.serve_component`** builds a whole `wasi:http` 0.2 component that `wasmtime serve` runs as it is, with `handle_stream`, trailers and `http.Client` inside it (decision 0447).

## Language

- **Valued enums** (decision 0269): `enum Code: Int { Ok = 200, Missing = 404 }` and `enum Level: Str { Low = "low" }`, with `value()`, `E.from(v)`, `E.all()` and Json by value; a `const` in a pattern matches by equality (XO0430 for a misspelled one); a C `enum` binds as a valued enum.
- **Effects:** an effect variable shared by several callback parameters is the union of their effects (decision 0432), and effects can no longer leak through a struct's effect parameter (a program that leaked one now fails XO0701).
- `List.filter_map`, `UInt64.from_bits` and `to_bits`; `${x:?}` uses a type's own `debug()`.

## Standard library

- **New:** std/deque, `backoff.retry_notify`, `read_mapped`, `Net.interfaces`, `Proc.usage`, `ProcOutput.usage`, `Child.wait_usage`, `Fs.disk_usage`, `debug.mem_trim`.

## Tools

- **`xo test` replays a passed run** (` (cached)`, decision 0439), links std's test variant (decision 0436), and parses only the std bodies it needs: an edit's `--std-module` run on linux/arm64 takes 0.58 (base64), 0.512 (url) and 0.406 (json) of the wall time it took before.
- **No silent success** (decision 0437): `xo test` names its skips, an unknown `--std-module` exits 2, `xo query` outside a module exits 2.
- **`xo vet --cost` reports `XO0915`**, a raw address used after its value was released in the same block (decision 0460); std's own code is checked with `--cost-std`.
- `xo bench --ab` runs 3 rounds by default and takes `--early`; `xo dev size --by-cause` shows a program's code by cause. On Windows an edit's link reads std from a cached half and the PDB is written while the program runs.

## Code size, speed and memory

- **xo's own code** (native arm64 `__text`, stage 2) from 71.4 MB at the cycle's first measurement to 32.8 MB: struct zeros from one function per type, shared landing pad tails, static zero images and retain thunks (the self build's wall time and max RSS each 0.843 of its base on linux/arm64).
- **Small builds use less memory:** a warm or edit build of the collections program on the Mac from 84 MB to 46 MB of RSS.
- **Linux huge pages follow density** by default (decision 0456): 2,000 held TLS connections under THP always take 63 to 64 MB RssAnon, against 125 to 138 MB before and 87 to 95 MB for Go.
- Runtime locks on Linux spin longer before a futex sleep (+3.1% req/s at 256 connections on linux/arm64); a list of elements over 256 bytes starts with one slot (300 TLS connections' save lists 2,200 to 560 kB).

## Fixes

Each names the first release that had the bug.

- **Security: a hostile archive crashed the program.** Native untar and unzip could fault on a crafted archive (a zip whose local header fell before the data, a tar or PAX time past 2262, a zip NTFS time with its top bit set) instead of returning an error; each now gives Go's answer, and several other malformed archives read as Go reads them. Found by a fuzz harness against Go's archive/tar and archive/zip. Since 0.11.0.
- **Memory read after it was freed**, found by a race stress and the new `XO0915`: a chunked request body and an HTTP/2 response's content-length (since 0.12.0); fault traces, crash reports and the runtime's error messages (every release with them); a back reference that grew inflated output and a WebSocket client frame's mask key (since 0.34.0); the resolver configuration and hosts file (since 0.35.0).
- **Native dial on Linux** could return a connection that was never made: since 0.7.0.
- **Windows:** asynchronous preemption could lose a preempted task's registers under load (since 0.25.0); `debug.mem_total` returned 0 (since 0.26.0).
- **Miscompiles (arm64):** a loop's `var` Bool set from a compare with a value read at the loop's top could end the loop after one turn (since 0.19.0); two Float stores to a list element paired past a read of the same element through another index (since 0.25.0).
- **Type checking:** a function could perform effects it did not declare through a struct's effect parameter (since 0.11.0).
- **HTTP:** a 431 reply closed without a lingering read, so the client could see a reset (since 0.12.0; 413 since 0.35.0); an HTTP/2 `handle_stream` request whose trailers arrived in a later read than its last DATA frame hung (since 0.25.0).
- **Runtime:** an idle main thread could lock the park of a finished inline task's freed context; LLVM split builds at -O2 could skip main's defers after a fault (both since 0.24.0).
- **Tools:** `XO_PROF=cpu` undercounted samples (on macOS about 0.14 of the CPU time) and the heap profile counted a grown big block at its first size (since 0.18.0); Linux `-s` builds kept Xo's function names (since 0.19.0); darwin LLVM builds in a directory with a comma in its path failed to link (since 0.29.0).
- **Go backend:** json decode now fails on a duplicate key's replaced value of the wrong type and on a number out of Float's range, matching Go and the native backends; `json.compact` and `json.indent` accept such numbers as Go does (since 0.7.0).
- **Go backend:** two builds of one program at the same time broke each other's build tree, and two `use go` binds at the same time failed XO1101 (since 0.7.0).
- The full list, with the release each bug shipped in, is in CHANGELOG.md.

## Idle connections against Go

Server memory for each idle keep-alive connection (RssAnon on the linux/arm64 offload host, bench/perf/idleconn; [decision 0449](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md)). Before this release an idle connection held a task on its own fiber stack: at 1,000 and 5,000 connections Xo used 38 and 17 KB for HTTP/1.1 against Go's 27 and 21 KB, and 127 and 91 KB for HTTP/2 over TLS against Go's 53 and 44 KB ([lines 15 to 20](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L15-L20)). Each stack also cost two memory mappings, so Linux's default `vm.max_map_count` capped a server at about 32,000 idle connections ([lines 35 to 38](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L35-L38)). With idle connections parked:

- **HTTP/1.1:** 40,000 idle connections take 255 MB, 6.4 KB each, in 293 mappings ([lines 202 to 205](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L202-L205)).
- **HTTP/2 over TLS:** 40,000 idle connections take 918,892 kB, about 23 KB each, in 470 mappings, against Go's 44 to 53 KB ([lines 224 to 228](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L224-L228)).
- **HTTP/1.1 over TLS:** 40,000 idle connections take 752,584 kB, about 18.8 KB each ([lines 259 to 261](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L259-L261)); Go was not measured in this mode.

The comparison has limits. Go's figures are from 1,000 and 5,000 connections, not 40,000. The TLS engine is about 19 KB of HTTP/2's 23 KB, and a parked connection keeps its 32 KB read buffer and its write buffers ([lines 228 to 232](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L228-L232)). Where Xo still holds more is slow keep-alive traffic: a plain HTTP/1.1 connection waits 20 ms on its own stack before it parks, which added 14 to 23 MB at 2,000 and 10,000 connections with 10 to 30 ms between requests ([lines 295 to 306](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L295-L306)); 0.36.0 also parks such a connection at once after its first linger times out, until it is busy again ([lines 313 to 318](https://github.com/xo-lang/xo/blob/main/knowledgebase/decisions/0449-idle-connection-memory.md#L313-L318)).

## Xo against Go

The release comparison runs the same programs in Go and in Xo on the release's own build (the v0.36.0 darwin/arm64 archive, Go 1.26.1, darwin/arm64 on an Apple M4 Pro, `xo bench --ab`, 3 interleaved rounds). Ratios are Xo's wall time over Go's; under 1.0 means Xo took less.

- **Compute:** interpreter 0.304, strings 0.532, sort 0.821, collatz 0.919, fib 0.968, sieve 0.973; n-body 1.056, still slightly slower than Go.
- **Collections:** branch 0.024, iter 0.280, putget 0.600, get 0.623, update 0.641, copymut 0.666, push 0.849 (was 0.731, see Known issues).
- **Concurrency:** mutex 0.271, spawn 0.443, fanout 0.686, ping-pong 0.877, rwmutex 0.892; parallel map 1.044 in a noisy 3-round run (1.01 to 1.45; 0.35.0: 0.988).
- **Tests:** `xo test --std-module` against `go test`: base64 0.566 cold and 0.815 after an edit, json 0.336 cold and 0.236 after an edit.
- **In all:** 22 of 24 rows faster than Go; n-body is slower, and the parallel map is 1.04 in a noisy run.
- **HTTP (linux/arm64):** /alloc serves 5.9x (32 connections) and 3.1x (256) Go's requests a second, /hello 1.29x and 1.84x; against 0.35.0, /hello is within 1.2% and /alloc 4 to 5% slower.
- **Builds:** a cold build of a small program takes 840 to 1040 ms against `go build`'s 1370 to 1410, and an edit rebuilds in 30 to 40 ms (the collections program 100) against Go's 90 to 100.

## Known issues

- **Shutdown hang (native backends, 0.36.0 to 0.40.0; found 2026-11-05):** a native HTTP server with idle keep-alive connections can hang on shutdown (SIGTERM, or a stop request) when a connection answered a request in the 20 ms before the stop; `serve` then waits until the grace period ends, or for ever without one. Fixed in 0.41.0.
- **List pushes on macOS:** a loop pushing into many lists that grow past 256 elements (bench/perf coll push) runs at 1.15x 0.35.0's time on macOS (0.95x on linux/arm64). The cause is decision 0452's list growth curve (Go's slice curve), which cut the compiler's own peak memory; a fix is assigned.
- **Peak memory of map-heavy programs:** coll putget's peak memory is 18% above 0.35.0's on macOS, from the 16 KiB size-class limit at start on darwin (ee90cd836), and 32% above on linux/arm64, mostly from the page blocks of the 16 KiB and larger size classes (decision 0425, 5523fb8dd); a fix is assigned.
- **/alloc in a 4 KiB page VM:** bench/perf/jitter's /alloc on a Linux VM on Apple silicon (4 KiB guest pages) runs at 0.966 and 0.953 of 0.35.0's requests a second at 32 and 256 connections, with about 4.5% more CPU a request. The same servers on macOS are level with 0.35.0 in requests a second, with 1 to 4% more CPU a request, and the cause costs under 1% on bare-metal Graviton 4; Xo's own part of it is fixed (decision 0459).
- **Windows preemption under load:** 8 of 256 runs of a preemption stress program beside CPU burners on windows/amd64 still print a wrong integer sum (none with `XO_PREEMPT=0`); the cause is open.

## Coming next

- Decision 0460 is accepted: the compiler will keep a value alive past the last use of a raw address taken from it, which closes the class of use-after-free bugs fixed above.

