krites is a tool for culling wedding photographs. You point it at a card full of frames, it throws out the blurred ones and the ones where someone blinked, and it hands the rest to my wife so she can get on with the actual work.
Version 0.2.0 spent thirty-eight minutes trying to build itself and then died compiling an OpenAI SDK. I found out at half past six in the morning, still in my dressing gown.
What the failure actually said
build failed: github.com/openai/openai-go/v3: compile: signal: killed
target=linux_amd64
signal: killed isn’t a compiler error. The Go compiler didn’t object to anything. It was executing perfectly happily and then the kernel’s out-of-memory killer walked up and shot it, which is what the operating system does when the machine is about to run out of RAM and something has to go. The build system sees a process vanish without a word and reports the only thing it knows: the thing died.
So there was nothing wrong with the code. There was something wrong with the room the code was being compiled in, which is not a distinction you think about until a machine makes you.
goreleaser was building four targets at once, in parallel, on my homelab runner: four Go compilers all going flat out, all holding the whole dependency graph in memory at the same time. That had been getting slower for weeks and I’d been ignoring it in the way you ignore a build that still finishes, which is to say completely, right up until it doesn’t.
An SDK it has never once called
And what it was compiling when it died was github.com/openai/openai-go/v3.
A photo-culling application, for a photographer, that runs its face and eye detection locally on a laptop and talks to nothing.
It has never once called that SDK. It doesn’t have a code path that could. The package is in the build graph because krites is built on go-tool-base, and go-tool-base’s root command wires in a chat feature, and that chat feature speaks to several providers, and one of those providers is OpenAI. So the SDK arrives, uninvited, through three layers of things being helpful, and then sits there being one of the largest things the compiler has to chew through.
Dependency weight has a compile-time cost, and nothing tells you about it. go build doesn’t report which package cost the most memory. The tests hadn’t got slower in any way I’d have spotted. It had been expensive for however many releases, entirely unremarked, and then surfaced as a runner falling over thirty-eight minutes into one, which is about the least convenient place available.
The fix is dull, which is fine
Cap goreleaser’s parallelism at one. Build one target at a time, stay under the memory ceiling, accept a slower release. That went in as a one-line change with a comment above it explaining why, and I checked while writing this: the comment is still sat there in the CI config, doing its job of stopping some future version of me from tidying away a parallelism setting that looks arbitrary (it isn’t, and now the file says so).
Slower, though! Serial builds are about as slow as you’d expect, roughly fifty seconds a target, and that’s kinda the whole trade: the release takes longer, and the release finishes.
What the OOM had been hiding
I deleted the failed release and its empty tag, re-pointed v0.2.0 at the fixed commit, and watched it go again.
The OOM was gone. All four Unix targets compiled cleanly, one after another, as intended.
And then it failed anyway, on something else entirely: the Windows build, where the CGO-free ONNX binding calls a loader function that only exists on POSIX systems. That break had been there the whole time. It had simply never been reached, because the parallel build ran out of memory and died before it ever got as far as Windows.
One bug had been stood in front of another the whole time, quite happily. Fixing the first didn’t cause the second, it just stopped shielding me from it. (That one turned into its own decision, because the answer wasn’t to fix the Windows build.)
v0.2.0 went out on the third attempt.
The parallelism cap is still in place, and the SDK is still in the graph. I went and checked while writing this, expecting to find it still riding along unused and to end on a promise to deal with it one day.
It isn’t quite freight any more, and the dates are almost rude about it. Eleven days after that release I was drafting a spec for an AI review feature, because Hailey had been pasting her photographs into Gemini for a critique and had gone and built the workflow herself out of whatever was lying around, which is an argument I’ve made at length elsewhere and won’t make twice. Twelve days after, the code landed: a critic that looks at her work and tells her what it thinks of the crop, built on the very chat client the SDK arrives with.
The SDK itself still hasn’t been called, even now. Hers runs on Gemini, behind the same adapter, and I’d not bet on that changing. But the client it belongs to is doing real work in the app, and moving her onto another provider is a config line rather than a rebuild, which is the whole reason that chat feature speaks to four of them at all. The freight turned out to be a spare.
That’s a bit galling, as endings go. The dependency I resented enough to build a whole post around was sitting there waiting for a feature I hadn’t thought of yet, and the only thing wrong with it was that it turned up first and charged me for the privilege of not needing it. I’m not sure that’s a lesson so much as an accident I benefited from… and I’d still rather have known what it cost me before a runner told me over breakfast.





