In June I wrote a decision document recommending we stay in Python and revisit a Go migration at twenty customers. It was well argued and I still think the reasoning was correct. In August I rewrote the whole thing in Go anyway. This is what changed, and why "I argued the opposite" is not an argument.
The June assessment
The question was how to package a container that runs inside someone else's cloud tenant while keeping the intellectual property from being trivially extracted. Two options: compile and obfuscate the Python, or rewrite in a compiled language.
The recommendation was Python, and the reasoning held up well:
- A rewrite was three to six months to reach functional parity, with real regression risk.
- The model vendor's Go SDK was considerably less mature than the Python one, particularly around prompt caching and streaming tool use.
- The cloud authentication libraries were battle-tested in Python and merely functional in Go.
- The intellectual-property advantage was real but modest, because string literals are extractable from any binary and the primary control — prompts delivered at runtime, never at rest — is language-independent.
- Cold start only mattered under scale-to-zero, and one warm replica made the languages equivalent at a cost already inside the modelled cloud bill.
Conclusion: launch on Python, revisit at twenty customers or on a hard scale-to-zero requirement. Every line of that is defensible and I would write it again with the information I had.
What actually changed
Not the facts. The weights.
The June assessment treated tamper resistance as an engineering preference — something you want, ranked against other things you want. By August it had become part of a commercial claim. The whole sales conversation with a security buyer runs through what this opaque binary is doing in my tenant, and the answer "it is a Python application with an obfuscation layer over it" is materially weaker than "it is a single static binary", not because the second is unbreakable but because the first invites a class of question the second does not.
Image size moved from a footnote to a real operational fact: roughly a gigabyte of Python runtime and dependencies against twenty to thirty megabytes of static binary, pulled into a fresh host in every customer tenant, on every deployment, forever.
And the SDK maturity gap — the strongest technical argument in the June document — had narrowed while I was busy elsewhere. Arguments made against a moving target expire, and nothing in my process was set up to notice that one had.
| Factor | June weight | August weight |
|---|---|---|
| Time to production | High — dominant | Lower — no customers waiting |
| Tamper resistance | Engineering preference | Part of the commercial claim |
| SDK maturity gap | High — decisive against | Narrowed materially |
| Image size and startup | Footnote | Per-tenant operational cost, permanently |
| Rewrite risk | High | Lower — thin dependencies, separated domain logic |
How the port ran
In nine waves, over roughly two weeks, with both trees alive simultaneously and the Go data models held byte-compatible with the Python ones so that audit records and state files written by one could be read by the other. That compatibility contract was the thing that made it survivable — at no point was there a flag day, and at every point there was a working system.
On the seventh of September the Python tree was formally dropped, the parity contract released, and new fields started being added to the Go models alone. Writing that decision down as a dated record mattered more than it sounds: until it existed, every new field was an implicit question about whether we were still maintaining two implementations.
What the rewrite actually cost
The hard parts were exactly where you would predict and nowhere else: query builders with accumulated schema knowledge, authentication plumbing, and the dozens of small behavioural details that were not written down anywhere because they lived in the Python code as the only specification.
That last category is the real tax on any rewrite. Not the code — the undocumented decisions embedded in it. Every one of them has to be rediscovered, usually by finding out at runtime that something no longer works the way it used to.
The meta-point
A well-argued decision is not a permanent one, and having argued a position publicly and carefully creates a real pull toward defending it. That pull is the thing to watch for. The June document was not wrong; it was correct under June's weights, and weights change because the business changes around them.
What I would do differently is small and concrete: when writing a decision document, name the specific conditions that would reverse it, and date them. My June assessment did include revisit triggers — twenty customers, or a hard scale-to-zero requirement — and the actual reversal came from none of them. It came from tamper resistance being promoted from preference to sales requirement, which was not on the list because I had not yet understood which conversation would decide the sale.
The triggers you can write down are the ones you already know about. The one that gets you is the variable that changes category.