← Notes

One build flag, 20× faster

August 4, 2026 · Notes from building Recilic

The morning after the first mesh came alive, it was pushing a 61.9 GB photo library across the room at 1.2 MB/s. Fourteen hours to finish. By breakfast it was doing 23.8 MB/s — and the fix that mattered was embarrassingly boring.

Live mesh at 23.8 MB/s between a MacBook and a Mac mini

The suspects

The obvious theories were all about the protocol. Every shard was written strictly one at a time — send, wait for the peer's acknowledgement, send the next — so throughput was capped by round-trip latency rather than bandwidth. With ~19,000 photos, nearly every file is a single chunk whose shards each pay that full round trip. Both true, both worth fixing. Neither was the story.

Then came the question that cracked it: "wait — are these two Macs even talking over the LAN, or are they going out through the VPN?" Checking meant measuring the raw link, so: pipe 100 MB over SSH between the same two machines. 20 MB/s. The network was fine. Recilic was using six percent of a road it already had.

The unglamorous answer

The dev builds were Debug builds. So I benchmarked the hot path in both configurations, 8 MB through each stage:

erasure-encode (our Swift): 3.2 MB/s → 142.9 MB/s  (44×)
ChaChaPoly seal (CryptoKit): 958.8 → 958.5 MB/s  (1.0×)
SHA-256 (CryptoKit): 3260 → 3156 MB/s  (1.0×)
chunk split (our Swift): 1965 → 2080 MB/s  (1.1×)

The entire deficit lives in one place: the GF(256) erasure coder. The crypto was never slow — CryptoKit ships pre-optimized inside the system, so your build settings can't hurt it. But the coder is our Swift, multiplying every single byte through a lookup table. With optimizations on, gfMul inlines into a tight loop, the Data retain/release traffic disappears, and whole-module optimization inlines the tables across files. With -Onone, every byte pays a real function call plus reference counting.

Why a build flag can be worth 44×

Imagine you have to stamp 66 billion envelopes. That's roughly what the coder faces: 61.9 GB is 66 billion bytes, and it multiplies every one of them four times over — a few hundred billion tiny operations. Each operation itself is trivial: look up two numbers in a table, combine them. It's the paperwork around each operation that decides your afternoon.

Debug mode is doing it like this: for every single envelope, walk to the supply room, fetch the stamp, walk back, stamp it, return the stamp, then write a line in a logbook. The stamping is instant. The walking is everything.

Release mode keeps the stamp in your hand and throws the logbook away.

Translated back: the walk to the supply room is the function call, which the optimizer inlines straight into the loop; the logbook is automatic reference counting, which the optimizer proves unnecessary and deletes. Neither changes what the program computes — only how much ceremony surrounds each byte.

And that's why Apple's crypto is immune: it shipped sealed, already in stamp-in-hand mode. Your build settings can't reach inside it. Only the code you write yourself pays the debug tax — and it pays it once per byte.

And the numbers line up exactly: a 3.2 MB/s coder, minus I/O and framing overhead, is the 1.2–2 MB/s we were seeing. A 142.9 MB/s coder is far above the 20 MB/s network — so the bottleneck moved to the wire, which is where it belongs.

Rebuilt with optimizations: 1.2 → 23.8 MB/s, which is above the 20 MB/s we measured over SSH. The backup that needed fourteen hours now needs about forty-five minutes. The bottleneck is the Wi-Fi link, exactly where it should be.

The fixes that were still worth making

The protocol theories weren't wrong, just outranked — and they shipped anyway, because at line rate the remaining latency actually matters:

Concurrent shard writes. A chunk's six shards are now written in parallel instead of serially. If one fails, the whole group is still drained so every shard that landed gets registered for cleanup — a partially-written backup must never leave orphans behind.

A connection pool. Four channels per peer, so concurrent writes overlap their round trips instead of queueing behind one pipe.

Two papercuts from real use. A laptop that slept mid-transfer used to hang the backup forever on a dead socket — now TCP keepalives plus per-request deadlines surface it as "peer unavailable" within a minute, and the next backup reconnects. And the first connection after launch could fail once while macOS rebuilt its local-network permission cache; it now retries itself, so nobody sees it.

One bug fixed for good measure: pressing "Back Up Now" while a prerequisite was missing did nothing at all — silently. A button that looks clickable and answers with silence is worse than an error message. It now says exactly what's missing.

The lesson, written down

Never diagnose a performance problem in a Debug build. Measure the raw link before blaming your own protocol — and when someone asks a naive-sounding question like "are we even on the right network?", go measure it. That question was worth 20×.

Recilic 0.1.0 is on the Mac App Store at a founding price of $9.99. The mesh ships as a free update — and yes, at line rate.