# Xo 0.41 release notes


**Downloads:** [Xo 0.41.0 on GitHub](https://github.com/xo-lang/xo/releases/tag/v0.41.0),
with archives for macOS (Apple silicon and Intel, signed and
notarized), Linux (arm64 and x86-64), and Windows (x86-64, Authenticode
signed), and a `SHA256SUMS` file.
[Getting started](/docs/getting-started/#install) shows how to install
one. These notes cover everything since 0.40.0.

Xo 0.41.0 fixes a native HTTP server that could hang on SIGTERM (0.36.0 to 0.40.0), checks a dependency's function bodies only as a build reaches them, so builds of large module graphs take less memory, and makes the edit loop faster: a one-line compiler edit rebuilds xo in 10.7 to 13.7 s on the Mac instead of 49 to 65 s. std gains CBOR with `derive Cbor`, and `/debug/sched` reports open fds and pending timers for long-running services.

Upgrade if you run a native HTTP server with keep-alive clients. One change can stop a program from compiling: `cbor.CborErr` has a new variant, so an exhaustive `match` on it needs an arm for `Type`.

## Breaking changes

- **std/cbor: `CborErr` has a new variant, `Type(msg)`**; a `match` on `CborErr` that lists every variant fails to compile until it handles `Type`.

Other changes you may notice:

- **`xo build` and `xo run` check a dependency's function bodies only as they reach them** (decision 0517): a type error in a dependency function the program never calls no longer fails the build. `xo check`, `xo test` and `xo vet` of the dependency still report it, and `XO_DEP_LAZY=0` checks every body. s3 + dynamodb on linux/arm64: peak RSS 932 to 631 MB, cold 4.44 to 4.01 s.
- **`xo get` checks each newly required module version once** and prints a warning with its first errors; the requirement is still added. `XO_GET_CHECK=0` skips it.
- **`xo run DIR/main.xo` runs the program of DIR** instead of stopping with a usage error.
- **A native HTTP server's responses written after a stop request carry `Connection: close`**, as net/http's after Shutdown.
- **`xo check DIR` runs the native support check by default** (decision 0523): every function of the named directories' modules, a library's too, so what a native build would refuse (XO1201) shows at check time. A walk (`DIR/...`) runs it only with `--support`; `--support=false` skips it. A `use go` program now fails plain `xo check`, as its native build does; `--backend=go` checks it by the Go backend's rules.

## Fixes

- **A native HTTP server no longer hangs on SIGTERM or a stop request** when a kept-alive connection answered a request in the 20 ms before the stop (arm64, amd64 and LLVM backends; 0.36.0 to 0.40.0).
- **Native backends run more function values:** a function value with `var` parameters (a named function passed as a value, a stored closure, one that fails with `?`), `Mutex.with` given a function value that is not a closure literal, and a value whose type differs only in effect arguments (`Request[env]` as `Request[fx]`) now build and run; each said XO1201 where the Go backend ran it.
- **Effects:** a type with an effect parameter implements an interface with one when the arguments agree (`ProcProvider[env]` as `Provider[env]`), where it was XO0701.
- **WebAssembly:** a function value or closure with a parameter or result of more than 256 leaves is called by memory, so such programs build and run on wasip1 and wasip2.
- **Go backend:** a std method with a `var` receiver in a module the Go backend compiles from its native implementation builds (it said there was no route).
- **`xo serve`:** a cached check no longer keeps answering its old result after an edit to another directory of the module.
- **`xo check --support` of a large workspace** no longer holds every module in one checker (aws's 438 modules passed 18 GB), and no longer reports a false XO1201 for a shared module's bodies.
- **A syntax error in a lazily checked dependency body** is reported at its file and line and fails the build; it was a compiler fault.

## Standard library and runtime

- **std/cbor** (decision 0527): `derive Cbor`, `cbor.marshal[T]`, `cbor.unmarshal[T]` and `cbor.unmarshal_prefix[T]` with fxamacker/cbor v2's rules and tags (`key,omitempty,keyasint,toarray`); 2,000 typed cases agree with the oracle.
- **std/amqp** (decision 0521): `message_format` on send (Service Bus batches), manual credit (`credit: 0`, `issue_credit`, `drain`), `ReceiverSettleMode.Second`, annotations on `modify`, and `remote_attach()`.
- **std/sql's memory driver** indexes UNIQUE INT and TEXT columns and writes tables in place (decision 0528): on linux/arm64 a lookup by UNIQUE column on 10,000 rows takes 0.002 of its time before and INSERT plus DELETE 0.003, and the notes service serves 1.58 times the requests at saturation.
- **For long-running services:** `/debug/sched` reports `fds_open` and `timers_pending`, and `XO_CRASH_FILE=PATH` appends a fault report with its traceback to PATH as well as standard error (Linux and macOS), like Go's debug.SetCrashOutput.
- **Heap:** 2 and 4 KiB buffers take 1.27 times their size instead of 2.01, and sizes from 1 to 8 KiB 1.03 to 1.06 times instead of up to 1.41 (decision 0506).
- **Profiling:** a CPU profile holds 262,144 samples instead of 32,768, and one that still fills its ring says PROFILE INCOMPLETE with the share lost and the XO_PROF_HZ that would keep them all.

## Speed and memory of the toolchain

- **Edit loop:** a one-line compiler edit rebuilds xo through xobuild in 10.7 to 13.7 s on the Mac instead of 49 to 65 s; on linux/arm64 a warm one-line compiler edit went from 17.5 s to 8.2 to 8.3 s wall now that a cc link reuses unchanged objects (121 of 127).
- **Warm builds:** the first edit after a cold build is warm (s3 + dynamodb: 0.34 s, was 1.17 s), the second reuses the caches of the one before (0.33 s, was 0.58 s), and a cold build is no longer slowed by the background build of std (4.50 to 4.56 s, was 5.22 to 5.36 s).
- **Build memory:** a cold build of s3 and dynamodb peaks at 822 MB instead of 932 (1,108 MB two steps back).
- **Compiler:** a cold build of xo takes 0.972 of the wall and 0.956 of the CPU of before, output identical.
- **Editor:** `xo lsp` uses `xo check`'s module check cache (first diagnostics on compiler/ssa/build.xo 537-758 to 355-399 ms), references take 142 to 148 ms instead of 622 to 733, and `xo def`, `refs`, `type`, `symbols` and `doc` are answered by a running `xo serve`.

## Diagnostics

- An XO0701 that asks for an effect also names the callers that will need it, so one round of edits fixes the chain; XO0701, XO0702, XO0202, XO0401 and XO0502 teach the threaded effect form (guide: knowledgebase/guides/effectful-closures.md); `parse_int` and friends point to `Int.parse`; an unknown field or method lists what the type has.
- A type mismatch (XO0401) between two types that print alike, such as a module's own `struct Conn` against the prelude's `Conn`, names each by its module path and notes where the module's type hides the prelude's; it read "expected `Conn`, found `Conn`".
- A `${...}` whose closure spans lines inside a one-line string is one error at the `${` saying to bind the value with `let` first (it was five errors ending at the closing brace), and an enum variant field written as a bare type (`Create(Str)`) says the field needs a name, with a fix that adds `value: `.

## Xo against Go

Measured before the cut (rvg-precut/report.md, Go 1.26.1, main f5c60ea35):

- **macOS arm64 programs**: ahead of Go on 15 of 20 run rows by wall (an interpreter 0.32 of Go's time, collection iteration 0.28, a mutex 0.32, spawn 0.44), level on four, one slower (nbody 1.036); every row at 0.29 to 0.85 of Go's peak memory.
- **Tests of std** (`xo test --std-module` against `go test`): json cold 0.395 of Go's time and 0.318 after an edit; base64 0.662 and 0.788. After an edit xo's test run peaks higher (base64 1.94 times Go's memory, json 1.30).
- **Builds**: cold 1.05 to 1.33 s against Go's 1.55 to 1.90 s, an edit 40 to 110 ms against 100 to 130 ms.
- **HTTP server** (linux/arm64): `/hello` serves 1.29 times Go's requests per second at 32 connections and 1.81 times at 256; a JSON response (`/alloc`) 5.94 and 3.12 times, and at a fixed 20,000 requests/s its p99 is 0.22 of Go's. Every row is level with 0.40.0.
- **linux/amd64** (micro13, `--release`): ahead of Go on five of seven programs (JSON 0.39 of Go's time, string building 0.55, fib 0.62), spawn100k level; JSON's peak memory is 3.3 times Go's and map1M's 1.41 (being worked on). Level with 0.40.0 on every row.
  - *Note (2026-11-08):* kept as history; skewed by WSL `/mnt/c` exec cost (about 6 ms a run, more for Go's larger binary), see the [ext4 rerun on the 0.44 page](/releases/0.44/#xo-against-go).

## Known issues

- A native HTTP server's memory grows with the number of requests in flight: a response's text that grows through the heap's size classes leaves a free block in each, and Linux is not given those pages back, so the notes service holds more memory at 128 to 256 concurrent connections than at 8 (its Go twin stays flat). A fix to the JSON list encoder and a scavenger proposal (decision 0533) are in progress.
- The full list is in CHANGELOG.md.

