Back to the report

🤖 What the TodoMVC bakeoff tells us

CARLOS and Carloku aren't tied to Go. We built the same small app in nine language setups, and eight of them shipped as a single static ARM executable that runs on Carloku with no changes to the platform. The ninth, Mojo, couldn't make a self-contained executable, so Nim took its place.

The app is TodoMVC, a small to-do list. The server builds every page, every action works with JavaScript turned off, and a little JavaScript on top makes it feel like the familiar TodoMVC. One AI model, Opus 5.5, built each version from the same brief. A second, Astra, reviewed each one, and the builder got one round to fix what it found. A third, Fable, scored how readable the code is. It was told the language but not which app it was reading. Every app ran on Carloku's default instance size. The charts are on the report page, and the apps, tests and measurement tools are in the code repository.

The numbers that matter

SetupPage loads a secondToggles a secondMemory at rest / peakExecutableCold build / one-line rebuildCost to build
Go34981017 / 71 MB12.3 MB10 s / 0.1 s$3.69
Go + Rastrillo33630919 / 63 MB14.1 MB10 s / 0.1 s$4.67
Rust904 (1,398 uncapped)1,02214 / 53 MB3.2 MB50 s / 5.8 s$2.92
Zig1,3823,1132.5 / 25 MB1.9 MB152 s / 5.4 s$5.71
OCaml9921,05811 / 17 MB8.2 MBemulated$7.65
Haskell40086211 / 77 MB28.7 MBemulated$5.06
Swift5371,45324 / 33 MB22.0 MB81 s / 3.9 s$4.60
Nim1,4913,5613.5 / 11.5 MB1.8 MB34 s / 0.9 s$4.84
Spinel7222,4518 / 13 MB8.7 MBemulated$10.70

No single language came out best. Go is the practical default. Nim and Zig used the least memory and served the most requests. Rust kept pace on speed where the test let it, has the most mature server stack of the three, and was the cheapest to build with AI. How hard it was to get a static ARM executable mattered more to cost and effort than the language did.

Correctness

Every app passed all 67 shared tests: TodoMVC's own 29 tests run with JavaScript on and again with it off, plus checks for unsafe input, very long titles, one visitor reaching another's list, and a screenshot comparison with TodoMVC's page. After the fix round, the review left between zero and three minor findings per app.

The two security problems were the same bug. A title containing an invalid UTF-8 byte crashed the whole server, in Zig and in Nim, and both were fixed in a second round. Our shared tests only send valid UTF-8, so they missed it and the reviewer caught it. The next bakeoff's tests should send broken input too.

Building with AI

Building cost between $2.92 (Rust) and $10.70 (Spinel), and took the builder between 15 minutes (Go) and 87 minutes (Spinel). The expensive builds were the ones where a static ARM executable was hard to get. Spinel's builder spent most of its time on cross-compiling, and OCaml and Haskell had to be built under emulation, with software on our x86 machine pretending to be an ARM machine. None of this says a language is harder for an AI to write.

The code itself came out similar everywhere. The apps run from 566 lines (Spinel) to 1,058 (Zig), and readability scores ranged from 7 to 9 out of 10 with most at 8, so they barely separate the languages.

Speed under load

The load test loaded a 50-item page while doubling the number of simultaneous connections every 10 seconds, then did the same for writes, ticking a to-do on and off. Reads fell into three bands: Nim and Zig at about 1,400 to 1,500 page loads a second, Rust, OCaml and Spinel at about 700 to 1,000, and Go, Go with Rastrillo, Haskell and Swift at about 340 to 540. Writes spread wider, from Nim's 3,561 toggles a second down to Go with Rastrillo's 309.

Put that in proportion before reading much into it. The slowest, Go at 349 page loads a second, is still about 29 million a day. Go's read rate is probably down to its SQLite library, a pure-Go translation chosen so the build needs no C compiler, though we didn't measure that directly.

Rust's numbers need a footnote. In its first read ramp and its write ramp, Rust was still speeding up at 64 connections when Carloku's edge started turning requests away, so the test never found its limit. In the one ramp where the edge didn't step in, the reads after the writes, Rust reached 1,398 page loads a second, close to Nim's 1,491.

That edge limit shapes everything above 64 connections. The edge is Carloku's set of front servers that receive traffic from the internet. When a request arrives at an edge server that isn't the one running the app, the edge allows at most 64 such requests in flight at once and answers the rest with 503, "try again shortly". Whether it applied depended on how our traffic was routed, so results above 64 measure the platform as much as the app.

Some apps failed outright under very heavy load, which the edge reports as 502: Swift at 512 connections and Nim at 256. Spinel's server process exited 16 times during its test and Carloku restarted it each time. In one 10-second Spinel step, no request completed at all.

Waking up and the first byte

Carloku puts an app to sleep after 15 quiet minutes. Every app answered its first request after sleeping in 1.2 to 1.45 seconds, and Carloku's own logs show about one second of that is the platform starting the process. The language barely matters.

The first byte of a page arrived after about 50 to 100 milliseconds. Our test machine reached Carloku over the internet, and the network's base delay drifted between about 17 and 37 milliseconds during the runs, so the two clusters, near 50 and near 95 milliseconds, say more about when each app was measured than about the apps.

Memory and size

Carloku doesn't show members how much memory an instance uses, so we ran each app on our own x86 machine and read it from the operating system. We've asked for the figures in carlos/platform discussion 365. At rest, the apps used 2.5 MB (Zig) to 24 MB (Swift). Under 64 simultaneous connections, Go and Haskell, both garbage-collected, peaked highest at 71 and 77 MB, with Rust next at 53 MB, while Nim, Spinel and OCaml stayed under 17 MB. Every app used a small fraction of an instance's memory limit.

Executables ranged from 1.8 MB (Nim) to 28.7 MB (Haskell).

Builds

These times are for the ARM executable that ships. A cold build compiles every library from already-downloaded source. A one-line rebuild adds one line to the code that serves the page, with everything else already compiled. Go takes about 10 seconds cold and 0.1 seconds to rebuild, with or without Rastrillo, which makes its edit-and-rebuild loop its clearest advantage. Nim follows at 34 seconds and 0.9 seconds.

OCaml, Haskell and Spinel were built under emulation, so their times don't compare with the others: 18 to 19 minutes cold for OCaml and Haskell, and 104 seconds for every Spinel rebuild, because Spinel compiles the whole program each time.

Choosing a language for a CARLOS app

Start with Go. It builds a static ARM executable in about 10 seconds, rebuilds after a small edit in a tenth of a second, and the platform's own tools and Rastrillo, the CARLOS web framework, are written for it. Rastrillo matched plain Go on page loads and code size but managed 309 toggles a second against plain Go's 810. In exchange you get its routing, sessions and migrations, which save design work as an app grows.

Use Nim or Zig when memory, executable size and request rate matter. Both made executables under 2 MB, used a few megabytes at rest, and served about four times as many page loads as Go. The catch is maturity: Nim's builder had to patch a copy of the standard library's basic HTTP server to listen on a Unix socket, and that server has no request timeouts.

Use Rust when you want a mature server stack without giving much away. It runs axum on tokio, reached 1,398 page loads a second when the edge didn't intervene, used 14 MB at rest, and was the cheapest to build with AI.

Whatever you pick, check the route to a static ARM executable first. A hard route costs more time and money even when the app's code is straightforward.

Where you'd use each

SetupUse it whenThink twice whenFrom the bakeoff
Go + RastrilloBuilding a real CARLOS product: sessions, migrations, identity, the family's conventionsYou need high write rates in a single instance336 reads/s, 309 writes/s; 10 s cold build, 0.1 s rebuild; the platform's own framework
GoYou want CARLOS's defaults without a framework, or a small service or toolYou'd end up rebuilding what Rastrillo gives you349 reads/s, 810 writes/s; same 10 s / 0.1 s builds; 1 dependency
RustOne instance takes heavy traffic, and you want a mature, production server stackFast edit-and-rebuild matters more than speed in production1,398 reads/s uncapped; 14 MB at rest; 3 MB binary; never failed under load; cheapest to build ($2.92)
NimFootprint is everything: many busy tenants per box, tiny binariesIt's exposed to the internet and needs a hardened server (timeouts, keep-alive limits)Top throughput (1,491 reads/s, 3,561 writes/s); 3.5 / 11.5 MB memory; 1.8 MB binary; patched stdlib server; 502s at 256 connections
ZigSame as Nim, with a C-like control over memory and almost no dependenciesYou want a stable language to maintain for years (0.x; standard library APIs still change)1,382 reads/s, 3,113 writes/s; 2.5 MB at rest; 1.9 MB binary; an invalid-UTF-8 crash caught in review
OCamlYour team likes strong types and wants low, steady memoryYou need native ARM builds without an emulator or a native ARM machine992 reads/s; peak memory only 17 MB; 18-minute emulated cold build
HaskellYour team already writes Haskell and values its type systemBuild times and binary size matter400 reads/s; 77 MB peak (the highest); 29 MB binary; 19-minute emulated cold build, 27 s rebuild
SwiftYour team writes Swift (say, sharing models with an iOS app)Memory at rest matters (it had the highest)537 reads/s, 1,453 writes/s; 24 MB at rest; 22 MB binary; 81 s cold build
SpinelExperiments, and Ruby developers curious about compiled RubyAnything in production yet722 reads/s; tiny memory (13 MB peak); but the process exited 16 times under load; whole-program rebuild of 104 s; the costliest build ($10.70)
MojoNot on Carloku today—Can't make a single self-contained binary; platform Idea 360

What it means for Carloku

Mojo couldn't ship because its runtime comes only as shared libraries, which led to Idea 360: binary releases that carry their own shared libraries. Members can't empty or reset an instance's database, and deleting an instance parks its hostname until an operator purges it, which is Feature Request 364. Members also can't see an instance's memory use (discussion 365).

Three smaller things came up. carlos vet is listed as a member command but needs operator access. The health column in carlos instances list lags by minutes, while the platform's own log events are accurate. And the edge's 64-request limit shapes any load test run from one machine. None of this stopped the apps from running, but it limits what members can measure and diagnose.

Caveats

Each language got one build, and another run could come out differently. One test machine measured the live apps over the internet, on the default instance size only. Three apps were built under emulation, and memory was measured on x86 rather than on Carloku.