<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Simon&#39;s Blog</title>
    <link>https://simoncrowe.blog/</link>
    <description>Software and Other Systems</description>
    <pubDate>Mon, 26 Oct 2026 14:07:12 +0000</pubDate>
    <item>
      <title>If AI Writes the Side Projects, How Do Engineers Level Up? An Experiment in Git Archaeology</title>
      <link>https://simoncrowe.blog/if-ai-writes-the-side-projects-how-do-engineers-level-up</link>
      <description>&lt;![CDATA[Five years ago, writing some software in an unfamiliar language in my spare time might have had some value beyond hobbyism. I’d learn new syntax, new idioms, and hopefully some best practices. Now I&#39;m reconsidering where to focus my efforts. AI agents can write better code than I can in most languages. Churning out working code isn’t anything special. The skills that seem to matter most now are architecture and judgement: the things that make an engineer seniorsup1/sup. You learn these skills best from complex projects with real production users.&#xA;&#xA;In this post, I’m exploring the idea that using AI agents to map out the development of prominent open-source codebases can develop judgement in ways that would have required becoming a long-time contributor in the past. The kind of judgement I’m looking at is:&#xA;&#xA;Which interfaces and abstractions earn their place as a project evolves?&#xA;Where do the trade-offs lie, even for sound decisions?&#xA;How does the concurrency model hold up as things get complex and scale?&#xA;What automation and processes can tame a project’s complexity?&#xA;&#xA;This is the thesis I’m testing: While AI agents take with one hand by making hobby coding even less valuable, they can give with the other by helping us glean wisdom from open-source projects.&#xA;&#xA;I chose to look at a Go project because while I’m not very familiar with the language, it&#39;s relevant to my current role as a platform engineer. Most of the technologies I use daily are written in Go. Notably: Docker, Kubernetes, OpenTofu (Terraform fork), and the OpenTelemetry collector. I’ve written some Go on weekends: Advent of Code and the kind of low-stakes projects that don&#39;t seem to matter now. I may have nearly a decade of commercial software engineering experience, but I lack Go-specific battle scars. For instance, I&#39;ve never had someone complain that a Go interface I wrote years ago is now causing them problems, as I have with the public API of Python modules.&#xA;&#xA;Below I ask an agent to summarise the state of etcd (the distributed key-value store underpinning Kubernetes) at key versions, then synthesise it into a narrative of how the system developed in response to user needs and production scale. This is a jumping-off point for exploration and thinking. I acknowledge the risk of regurgitating AI-generated text without truly understanding it, so I endeavour to explore the threads I write about independently.&#xA;&#xA;What I Learned from etcd&#xA;&#xA;I started with some AI-assisted git archaeology. I promptedsup2/sup Claude Code (Opus 5) to generate a report on each of four etcd versions, v0.1.0 to v3.7.1. I read the individual reports, which were overwhelming because they contained more plausible observations than I could ever hope to verify. To find a thread running through all versions, I used a second promptsup3/sup, along with the reports, to generate a summary. The only thing I’ll directly quote from the summary is this table. It shows, at a high level, how the project grew and when each API version arrived and left.&#xA;&#xA;Non-test, non-generated, non-vendored Go lines, measured per tag&#xA;(git ls-tree -r tag filtered to .go, excluding test.go, .pb.go, and&#xA;thirdparty/, Godeps/, cmd/vendor/, /vendor/):&#xA;&#xA;| | v0.1.0 | v2.0.0 | v3.0.0 | v3.7.1 |&#xA;|---|---|---|---|---|&#xA;| Whole tree | 3,204 | 16,909 | 57,860 | 110,019 |&#xA;| Consensus library | 3,798 (vendored go-raft) | 2,034 (raft/) | 3,666 (raft/) | 0 (external module) |&#xA;| v2 keyspace state machine | 1,203 | 1,907 | 2,212 | 2,231 (no API) |&#xA;| v3 keyspace state machine | — | — | 3,346 | 3,962 |&#xA;| Watch | 129 | 434 | 988 | 1,180 |&#xA;| Lease / TTL | (inside store) | (inside store) | 651 | 1,522 |&#xA;&#xA;What stands out most in this table is that the Raft consensus implementation has had three homes: first as a vendored library, then in the repository, and finally spun out into a separate repository.&#xA;&#xA;Spinning Out a Pure Raft library&#xA;&#xA;Raftsup4/sup is the consensus algorithm that keeps the keys and values stored in etcd consistent across multiple nodes. Raft means production Kubernetes clusters can deploy their data store across multiple nodes in a highly available configuration. Raft also underpins many of the guarantees etcd provides.&#xA;&#xA;A thread Claude identified from the initial v0.1.0 release to the recent v3.7.1 was the Raft consensus seam, which, as of today, connects the etcd server to a pure logic Raft library that leaves I/O to the consuming system. The same library, extracted from etcd, is used by other distributed databases including CockroachDB and Dgraph. &#xA;&#xA;In v0.1.0, there was a different external Raft library, and its interface wasn’t as clean. Raft wrote to the file system to update the log; it also called time.Sleep. By v2.0.0, this was replaced with an in-repo implementation: the I/O was factored elsewhere and time was represented as a logical tick rather than interacting with a system clock. (When attempting to understand the new Raft library, my puny meatbag brain found the simplified key-value store example code more helpful than current etcd source code.)&#xA;&#xA;At the time of writing, the Raft library README says: &#xA;&#xA;  Most Raft implementations have a monolithic design, including storage handling, messaging serialization, and network transport. This library instead follows a minimalistic design philosophy by only implementing the core raft algorithm. This minimalism buys flexibility, determinism, and performance.&#xA;&#xA;When writing software professionally, I try to extract business logic so it doesn’t contain side effects and I/O, favouring pure functions and dependency injection. Even in commercial software, this can be hard to achieve, and the payoff is often limited. For something like Raft, which has a PhD thesis devoted to it, I can imagine it was much harder. The payoff is greater too, as other distributed databases can and do use the system regardless of how they implement I/O.&#xA;&#xA;Not all software components can be pure, however. In etcd, they didn&#39;t delete the messy parts; they moved them to the etcd server.&#xA;&#xA;The Messy Details&#xA;&#xA;In version 0.1.0, etcd passed go-raft a directory path to write log entries to. This was paired with a callback function that applied log entries to etcd’s state.&#xA;&#xA;type Log struct {&#xA; ApplyFunc   func(Command) (interface{}, error)&#xA; file        os.File&#xA; path        string&#xA; entries     []LogEntry&#xA; results     []logResult&#xA; commitIndex uint64&#xA; ...&#xA;}&#xA;Source code here.&#xA;&#xA;The first external Raft library owned I/O and also owned the messy business of ensuring that all log entries were applied to the store. This is simpler in some ways because go-raft could commit changes to the log and apply them to the data store in the same function, managing concurrency with the same mutex.&#xA;&#xA;From version 2.0.0, even before it became a separate library, the Raft component contained only the business rules for keeping multiple nodes’ state consistent. The etcd server was now responsible for ensuring that the database’s state reflected the WAL entries. At this point, though, not much could go wrong. As in version 0.1.0, the state lived in memory, so if a node died, it would rebuild the state from a combination of a snapshot and the WAL.&#xA;&#xA;In version 3.0.0, the database state became durable. The release notes mention “new storage engine”. The part of this engine that stored key-value data on disk was Bolt (a fork of which is now maintained by the etcd project). While this change gave etcd better transaction isolation (the I in ACID) with MVCC, it also created a new durable record on every node: the key-value store in addition to the existing Raft WAL. This introduced a new problem when rebuilding an etcd node’s state: the etcd server needed to track the WAL offset that had so far been committed to the node’s key-value store. etcd committed to Bolt in batches (every 100ms or 10,000 writes), so after a crash the database could lag behind the log, and if it replayed already-applied entries, it could apply them twice.&#xA;&#xA;This is the commit message for saving that offset to Bolt: “etcdserver: save consistent index into v3 storage […] This helps to recover consistent index when restart in the future.” Consistent index doesn’t mean that the index is consistent, so much as that it’s the index of the latest committed Raft WAL entry that’s been written to the etcd server’s on-disk state. At this point, the design is sound: the index is written in the same Bolt transaction as the data, so after a crash both are at the same point, and etcd knows exactly where to resume. From v3 onwards, data consistency rests on this being true in all situations. &#xA;&#xA;Version 3.0.0 burdened the etcd server with ensuring that the consistent index agreed with what WAL entries had in fact been applied. It introduced a new class of potential bug which eventually manifested, resulting in error reports like this one. It is every Kubernetes user’s nightmare: “Some keys exists on node x and y but not on node z, and some on z but not on the other two, e.g. different pods list would be returned from different servers.” The root cause turned out to be this PR, which aimed to ensure the consistent index was saved on each commit. Unfortunately, there were a couple of unintended consequences:&#xA;&#xA;The in-memory consistent index value was shared state, read by any commit, not just the one carrying the matching data.&#xA;This in-memory value was updated before the transaction was committed to the key-value state (Bolt), creating a window in which a concurrent commit could persist it (e.g. the periodic commit that runs every 100ms).&#xA;&#xA;Saving the index and saving the data it pertained to no longer comprised an atomic operation.&#xA;&#xA;Postmortem, Pragmatism and Priorities &#xA;&#xA;The refactor that caused the bug was merged in May 2021 and shipped in v3.5.0 a month later. The first report of diverging etcd nodes arrived in December 2021. A maintainer confirmed the corruption in March 2022; the maintainers sent a public statement to the etcd and Kubernetes mailing lists, and the fix shipped in v3.5.3 in April 2022.&#xA;&#xA;This wasn&#39;t the first time this class of bug surfaced. In August 2016, two months after 3.0.0 shipped, a maintainer fixed the same failure mode in a different place (#6247). A snapshot routine could save the consistent index to disk after apply had updated it, but before apply had written the matching data. Their commit message describes the 2022 bug almost exactly, and ends by noting there were other places that needed the same care, &#34;but let us fix this first&#34;. A 2020 bug in the auth store (#11652) was another variation on the theme. The danger was known and named for five years. Each fix patched a particular symptom. Nothing turned &#34;the index is only saved alongside its data&#34; into something the code enforced.&#xA;&#xA;The 3.5.3 fix (#13854) finally did that. It split the in-memory value in two: an applying index for the entry currently in progress, and a consistent index updated only while holding the transaction lock, so it can reach disk only with the data it describes.&#xA;&#xA;The postmortem says the maintainers were “not aware [of] any cases of users reporting a data corruption in production environment”. They took the issue seriously all the same; databases survive on being trustworthy. What’s interesting about the postmortem is that most of the issues they identified were related to people and processes rather than code. Processes around releases had grown lax. Maintainers had moved on. The automated test suite was flaky and incomplete. They reproduced the bug partly by luck, because one maintainer&#39;s /tmp was mounted to a real disk rather than memory. The fix took over a week and involved multiple attempts. &#xA;&#xA;The work that followed the postmortem is what I find most instructive. They haven&#39;t undertaken a grand refactor of the EtcdServer god struct, which survives to this day, nor any of the other code smells the agent identified in its reports. I think this is because they had more important priorities. Instead, they built machinery for confidence. They reworked corruption checking. A robustness suite now injects failures and checks every run&#39;s history for linearisability. The suite’s README lists known correctness bugs, including those it has caught.&#xA;&#xA;Conclusion&#xA;&#xA;This post ended up being more about software engineering than Go. I think that&#39;s fine. The judgement I was hunting for is mostly language-agnostic.&#xA;&#xA;Some of this journey was reassuring in a way. Even a high-profile open source project that many depend on has TODO comments in the codebase, and fossils of old implementations that haven’t been cleaned up. Even here, it took a critical bug to restore release discipline and reliable tests. There was no instant fix. It took careful, coordinated work, much of which was later automated.&#xA;&#xA;I don’t want to overstate what I’ve learned. The initial raw AI reports were hard to follow and simply covered too much ground. I was out of my depth, and all I had were reams of plausible observations. The meat of this post came from a dialogue with Claude, where I tried to cut away everything but the most essential threads of etcd’s story. Without that, this post would likely never have been published. &#xA;&#xA;In domains I’m paid to work in, I guide the agent as much as it guides me. While jumping into an unfamiliar domain and relying heavily on the agent is fine for an experimental piece of writing like this, I wouldn’t feel comfortable being so far out of my depth if I were building software for others. Domain expertise still matters. After reading this, you wouldn&#39;t hire me to build a new database in Go. You’d pick a database engineer who can recite Designing Data-Intensive Applications_ from cover to cover and has shipped Go in production. I’ve gained some second-hand wisdom, and some trivia to bore people with. More valuably, I’ve gained a lasting suspicion of any invariant that lives only in tribal knowledge. Such invariants are even more dangerous now that agents can be dropped into a complex codebase with none of that context.&#xA;&#xA;Was it more useful than a toy coding project? Yes, but not by much. There are no shortcuts to expertise. Real fluency in a language still means working up from basic to advanced material until one can read unfamiliar code and run its logic in one&#39;s head, rather than relying on a maintainer&#39;s postmortem or an AI agent to explain it. That skill matters more now, not less. We&#39;re shipping more code we haven’t read, and sooner or later, even teams running fully automated software factories will need someone who can dive into an unfamiliar codebase and understand what it&#39;s doing. The scarcer that skill becomes, the more it will be worth.&#xA;&#xA;So the premise I started with was only half right. Hand-written side projects haven’t entirely lost their value. They&#39;re just less impressive. They’re like low-intensity cycling: nobody’s excited by it, but it builds the cardiovascular base that makes the hard sessions possible. Digging through a codebase like etcd is the hard session. Without the base, AI was my e-bike. I covered the distance, but my legs didn’t do most of the work. That’s fine for getting somewhere, but not ideal for getting fitter.&#xA;---&#xA;&#xA;span id=&#34;fn1&#34;1./span I’ve chosen the word senior because “expert” is hard to pin down. For instance, managers regularly judge seniority in the industry, though the technical bar for “senior” differs by org; some levelling frameworks try to formalise it. These frameworks cover much more than technical skill, but insofar as they cover the technical side, this from the senior (L50) section of this framework is a good example: “Has the skills to proficiently work on complex and unknown problems under pressure (e.g. debugging complex production issues at speed, or leading the resolution of critical or major incidents)”. Notice it doesn’t mention writing code. This kind of skill isn&#39;t automatable yet (though with enough telemetry, an agent can help investigate production issues).&#xA;&#xA;span id=&#34;fn2&#34;2./span The first prompt, run at v0.1.0, v2.0.0, v3.0.0 and v3.7.1: “Explore this codebase and outline its public APIs, its fundamental architecture and software design choices in a Markdown document: etcd-0.1.0.md. References to source code should support all points; priority should be given to where important abstractions are declared/implemented and where they earn their place and, if relevant, where they break down.”&#xA;&#xA;span id=&#34;fn3&#34;3./span The second prompt, used to generate a summary: “Read the markdown documents in the working tree. These all outline the design and trade-offs of etcd at a particular point in time. Generate another document that traces the development of the five most important systems or abstractions over the lifetime of the project. Once one area has been chosen, the source code at the four documented versions should be double-checked and referenced. Pay attention to trade-offs, management of complexity and how abstractions break down as well as design successes.”&#xA;&#xA;span id=&#34;fn4&#34;4./span I’m not a distributed systems expert, so I’m not going to try to summarise Raft beyond saying all nodes maintain write-ahead logs from which state can be built, all writes go through a single leader, and it employs a leadership election mechanism to survive faults. For a more detailed discussion of the Raft algorithm, you can read the original paper. To get some intuition of how leadership and elections work, there’s a web-based simulation here.]]&gt;</description>
      <content:encoded><![CDATA[<p>Five years ago, writing some software in an unfamiliar language in my spare time might have had some value beyond hobbyism. I’d learn new syntax, new idioms, and hopefully some best practices. Now I&#39;m reconsidering where to focus my efforts. AI agents can write better code than I can in most languages. Churning out working code isn’t anything special. The skills that seem to matter most now are architecture and judgement: the things that make an engineer senior<sup><a href="#fn1">1</a></sup>. You learn these skills best from complex projects with real production users.</p>

<p>In this post, I’m exploring the idea that using AI agents to map out the development of prominent open-source codebases can develop judgement in ways that would have required becoming a long-time contributor in the past. The kind of judgement I’m looking at is:</p>
<ul><li>Which interfaces and abstractions earn their place as a project evolves?</li>
<li>Where do the trade-offs lie, even for sound decisions?</li>
<li>How does the concurrency model hold up as things get complex and scale?</li>
<li>What automation and processes can tame a project’s complexity?</li></ul>

<p>This is the thesis I’m testing: While AI agents take with one hand by making hobby coding even less valuable, they can give with the other by helping us glean wisdom from open-source projects.</p>

<p>I chose to look at a Go project because while I’m not very familiar with the language, it&#39;s relevant to my current role as a platform engineer. Most of the technologies I use daily are written in Go. Notably: Docker, Kubernetes, OpenTofu (Terraform fork), and the OpenTelemetry collector. I’ve written some Go on weekends: Advent of Code and the kind of low-stakes projects that don&#39;t seem to matter now. I may have nearly a decade of commercial software engineering experience, but I lack Go-specific battle scars. For instance, I&#39;ve never had someone complain that a Go interface I wrote years ago is now causing them problems, as I have with the public API of Python modules.</p>

<p>Below I ask an agent to summarise the state of etcd (the distributed key-value store underpinning Kubernetes) at key versions, then synthesise it into a narrative of how the system developed in response to user needs and production scale. This is a jumping-off point for exploration and thinking. I acknowledge the risk of regurgitating AI-generated text without truly understanding it, so I endeavour to explore the threads I write about independently.</p>

<h2 id="what-i-learned-from-etcd">What I Learned from etcd</h2>

<p>I started with some AI-assisted git archaeology. I prompted<sup><a href="#fn2">2</a></sup> Claude Code (Opus 5) to generate a report on each of four etcd versions, v0.1.0 to v3.7.1. I read the individual reports, which were overwhelming because they contained more plausible observations than I could ever hope to verify. To find a thread running through all versions, I used a second prompt<sup><a href="#fn3">3</a></sup>, along with the reports, to generate a summary. The only thing I’ll directly quote from the summary is this table. It shows, at a high level, how the project grew and when each API version arrived and left.</p>

<p>Non-test, non-generated, non-vendored Go lines, measured per tag
(<code>git ls-tree -r &lt;tag&gt;</code> filtered to <code>*.go</code>, excluding <code>_test.go</code>, <code>*.pb.go</code>, and
<code>third_party/</code>, <code>Godeps/</code>, <code>cmd/vendor/</code>, <code>*/vendor/</code>):</p>

<table>
<thead>
<tr>
<th></th>
<th>v0.1.0</th>
<th>v2.0.0</th>
<th>v3.0.0</th>
<th>v3.7.1</th>
</tr>
</thead>

<tbody>
<tr>
<td>Whole tree</td>
<td>3,204</td>
<td>16,909</td>
<td>57,860</td>
<td>110,019</td>
</tr>

<tr>
<td>Consensus library</td>
<td>3,798 <em>(vendored <code>go-raft</code>)</em></td>
<td>2,034 (<code>raft/</code>)</td>
<td>3,666 (<code>raft/</code>)</td>
<td>0 <em>(external module)</em></td>
</tr>

<tr>
<td>v2 keyspace state machine</td>
<td>1,203</td>
<td>1,907</td>
<td>2,212</td>
<td>2,231 <em>(no API)</em></td>
</tr>

<tr>
<td>v3 keyspace state machine</td>
<td>—</td>
<td>—</td>
<td>3,346</td>
<td>3,962</td>
</tr>

<tr>
<td>Watch</td>
<td>129</td>
<td>434</td>
<td>988</td>
<td>1,180</td>
</tr>

<tr>
<td>Lease / TTL</td>
<td><em>(inside store)</em></td>
<td><em>(inside store)</em></td>
<td>651</td>
<td>1,522</td>
</tr>
</tbody>
</table>

<p>What stands out most in this table is that the Raft consensus implementation has had three homes: first as a vendored library, then in the repository, and finally spun out into a separate repository.</p>

<h3 id="spinning-out-a-pure-raft-library">Spinning Out a Pure Raft library</h3>

<p>Raft<sup><a href="#fn4">4</a></sup> is the consensus algorithm that keeps the keys and values stored in etcd consistent across multiple nodes. Raft means production Kubernetes clusters can deploy their data store across multiple nodes in a highly available configuration. Raft also underpins many of the <a href="https://etcd.io/docs/v3.7/learning/api_guarantees/">guarantees</a> etcd provides.</p>

<p>A thread Claude identified from the initial v0.1.0 release to the recent v3.7.1 was the Raft consensus seam, which, as of today, connects the etcd server to a pure logic Raft library that leaves I/O to the consuming system. The same library, <a href="https://github.com/etcd-io/raft/pull/1">extracted from etcd</a>, is used by other distributed databases including CockroachDB and Dgraph.</p>

<p>In v0.1.0, there was a different <a href="https://github.com/coreos/raft">external Raft library</a>, and its interface wasn’t as clean. Raft wrote to the file system to update the log; it also called <code>time.Sleep</code>. By v2.0.0, this was replaced with an in-repo implementation: the I/O was factored elsewhere and time was represented as a logical tick rather than interacting with a system clock. (When attempting to understand the new Raft library, my puny meatbag brain found the simplified key-value store <a href="https://github.com/etcd-io/etcd/blob/release-3.7/contrib/raftexample/main.go">example code</a> more helpful than current etcd source code.)</p>

<p>At the time of writing, the Raft library <a href="https://github.com/etcd-io/raft/blob/6263204f318c6fd5bacadc125067b65bd737e949/README.md">README</a> says:</p>

<blockquote><p>Most Raft implementations have a monolithic design, including storage handling, messaging serialization, and network transport. This library instead follows a minimalistic design philosophy by only implementing the core raft algorithm. This minimalism buys flexibility, determinism, and performance.</p></blockquote>

<p>When writing software professionally, I try to extract business logic so it doesn’t contain side effects and I/O, favouring pure functions and dependency injection. Even in commercial software, this can be hard to achieve, and the payoff is often limited. For something like Raft, which has a <a href="https://github.com/ongardie/dissertation/blob/master/stanford.pdf">PhD thesis</a> devoted to it, I can imagine it was much harder. The payoff is greater too, as other distributed databases can and do use the system regardless of how they implement I/O.</p>

<p>Not all software components can be pure, however. In etcd, they didn&#39;t delete the messy parts; they moved them to the etcd server.</p>

<h3 id="the-messy-details">The Messy Details</h3>

<p>In version 0.1.0, etcd passed go-raft a directory path to write log entries to. This was paired with a callback function that applied log entries to etcd’s state.</p>

<pre><code class="language-go">type Log struct {
 ApplyFunc   func(Command) (interface{}, error)
 file        *os.File
 path        string
 entries     []*LogEntry
 results     []*logResult
 commitIndex uint64
 ...
}
</code></pre>

<p>Source code <a href="https://github.com/etcd-io/etcd/blob/v0.1.0/third_party/github.com/coreos/go-raft/log.go#L21-L33">here</a>.</p>

<p>The first external Raft library owned I/O and also owned the messy business of ensuring that all log entries were applied to the store. This is simpler in some ways because go-raft could commit changes to the log and apply them to the data store in the same <a href="https://github.com/etcd-io/etcd/blob/v0.1.0/third_party/github.com/coreos/go-raft/log.go#L351-L401">function</a>, managing concurrency with the same mutex.</p>

<p>From version 2.0.0, even before it became a separate library, the Raft component contained only the <em>business rules</em> for keeping multiple nodes’ state consistent. The etcd server was now responsible for ensuring that the database’s state reflected the WAL entries. At this point, though, not much could go wrong. As in version 0.1.0, the state lived in memory, so if a node died, it would rebuild the state from a combination of a snapshot and the WAL.</p>

<p>In version 3.0.0, the database state became durable. The <a href="https://github.com/etcd-io/etcd/releases/tag/v3.0.0">release notes</a> mention “new storage engine”. The part of this engine that stored key-value data on disk was Bolt (a fork of. While this change gave etcd better transaction isolation (the I in ACID) with <a href="https://en.wikipedia.org/wiki/Multiversion_concurrency_control">MVCC</a>, it also created a new durable record on every node: the key-value store in addition to the existing Raft WAL. This introduced a new problem when rebuilding an etcd node’s state: the etcd server needed to track the WAL offset that had so far been committed to the node’s key-value store. etcd committed to Bolt in batches (every 100ms or 10,000 writes), so after a crash the database could lag behind the log, and if it replayed already-applied entries, it could apply them twice.</p>

<p>This is the <a href="https://github.com/etcd-io/etcd/commit/15ed6d8268e51459eed8a9ec3889ebe86490b041">commit message</a> for saving that offset to Bolt: “etcdserver: save consistent index into v3 storage […] This helps to recover consistent index when restart in the future.” <em>Consistent index</em> doesn’t mean that the index is consistent, so much as that it’s the index of the latest committed Raft WAL entry that’s been written to the etcd server’s on-disk state. At this point, the design is sound: the index is written in the same Bolt transaction as the data, so after a crash both are at the same point, and etcd knows exactly where to resume. From v3 onwards, data consistency rests on this being true in all situations.</p>

<p>Version 3.0.0 burdened the etcd server with ensuring that the consistent index agreed with what WAL entries had in fact been applied. It introduced a new class of potential bug which eventually manifested, resulting in error reports like <a href="https://github.com/etcd-io/etcd/issues/13766">this one</a>. It is every Kubernetes user’s nightmare: “Some keys exists on node x and y but not on node z, and some on z but not on the other two, e.g. different pods list would be returned from different servers.” The root cause turned out to be <a href="https://github.com/etcd-io/etcd/pull/12855">this PR</a>, which aimed to ensure the <em>consistent index</em> was saved on each commit. Unfortunately, there were a couple of unintended consequences:</p>
<ol><li>The in-memory consistent index value was shared state, read by any commit, not just the one carrying the matching data.</li>
<li>This in-memory value was updated before the transaction was committed to the key-value state (Bolt), creating a window in which a concurrent commit could persist it (e.g. the periodic commit that runs every 100ms).</li></ol>

<p>Saving the index and saving the data it pertained to no longer comprised an atomic operation.</p>

<h3 id="postmortem-pragmatism-and-priorities">Postmortem, Pragmatism and Priorities</h3>

<p>The <a href="https://github.com/etcd-io/etcd/pull/12855">refactor</a> that caused the bug was merged in May 2021 and shipped in v3.5.0 a month later. The first report of diverging etcd nodes arrived in December 2021. A maintainer confirmed the corruption in March 2022; the maintainers sent a public statement to the etcd and Kubernetes mailing lists, and the fix shipped in v3.5.3 in April 2022.</p>

<p>This wasn&#39;t the first time this class of bug surfaced. In August 2016, two months after 3.0.0 shipped, a maintainer fixed the same failure mode in a different place (<a href="https://github.com/etcd-io/etcd/pull/6247">#6247</a>). A snapshot routine could save the consistent index to disk after apply had updated it, but before apply had written the matching data. Their commit message describes the 2022 bug almost exactly, and ends by noting there were other places that needed the same care, “but let us fix this first”. A 2020 bug in the auth store (<a href="https://github.com/etcd-io/etcd/pull/11652">#11652</a>) was another variation on the theme. The danger was known and named for five years. Each fix patched a particular symptom. Nothing turned “the index is only saved alongside its data” into something the code enforced.</p>

<p>The 3.5.3 fix (<a href="https://github.com/etcd-io/etcd/pull/13854">#13854</a>) finally did that. It split the in-memory value in two: an applying index for the entry currently in progress, and a consistent index updated only while holding the transaction lock, so it can reach disk only with the data it describes.</p>

<p>The <a href="https://github.com/etcd-io/etcd/blob/00c789f5bca9df193763681407035d3ffbb0688e/Documentation/postmortems/v3.5-data-inconsistency.md">postmortem</a> says the maintainers were “not aware [of] any cases of users reporting a data corruption in production environment”. They took the issue seriously all the same; databases survive on being trustworthy. What’s interesting about the postmortem is that most of the issues they identified were related to people and processes rather than code. Processes around releases had grown lax. Maintainers had moved on. The automated test suite was flaky and incomplete. They reproduced the bug partly by luck, because one maintainer&#39;s <code>/tmp</code> was mounted to a real disk rather than memory. The fix took over a week and involved multiple attempts.</p>

<p>The work that followed the postmortem is what I find most instructive. They haven&#39;t undertaken a grand refactor of the <code>EtcdServer</code> god struct, which survives to this day, nor any of the other code smells the agent identified in its reports. I think this is because they had more important priorities. Instead, they built machinery for confidence. They reworked corruption checking. A <a href="https://github.com/etcd-io/etcd/issues/14045">robustness suite</a> now injects failures and checks every run&#39;s history for linearisability. The suite’s <a href="https://github.com/etcd-io/etcd/blob/v3.7.1/tests/robustness/README.md">README</a> lists known correctness bugs, including those it has caught.</p>

<h2 id="conclusion">Conclusion</h2>

<p>This post ended up being more about software engineering than Go. I think that&#39;s fine. The judgement I was hunting for is mostly language-agnostic.</p>

<p>Some of this journey was reassuring in a way. Even a high-profile open source project that many depend on has <code>TODO</code> comments in the codebase, and fossils of old implementations that haven’t been cleaned up. Even here, it took a critical bug to restore release discipline and reliable tests. There was no instant fix. It took careful, coordinated work, much of which was later automated.</p>

<p>I don’t want to overstate what I’ve learned. The initial raw AI reports were hard to follow and simply covered too much ground. I was out of my depth, and all I had were reams of plausible observations. The meat of this post came from a dialogue with Claude, where I tried to cut away everything but the most essential threads of etcd’s story. Without that, this post would likely never have been published.</p>

<p>In domains I’m paid to work in, I guide the agent as much as it guides me. While jumping into an unfamiliar domain and relying heavily on the agent is fine for an experimental piece of writing like this, I wouldn’t feel comfortable being so far out of my depth if I were building software for others. Domain expertise still matters. After reading this, you wouldn&#39;t hire me to build a new database in Go. You’d pick a database engineer who can recite <em>Designing Data-Intensive Applications</em> from cover to cover and has shipped Go in production. I’ve gained some second-hand wisdom, and some trivia to bore people with. More valuably, I’ve gained a lasting suspicion of any invariant that lives only in tribal knowledge. Such invariants are even more dangerous now that agents can be dropped into a complex codebase with none of that context.</p>

<p>Was it more useful than a toy coding project? Yes, but not by much. There are no shortcuts to expertise. Real fluency in a language still means working up from basic to advanced material until one can read unfamiliar code and run its logic in one&#39;s head, rather than relying on a maintainer&#39;s postmortem or an AI agent to explain it. That skill matters more now, not less. We&#39;re shipping more code we haven’t read, and sooner or later, even teams running fully automated software factories will need someone who can dive into an unfamiliar codebase and understand what it&#39;s doing. The scarcer that skill becomes, the more it will be worth.</p>

<p>So the premise I started with was only half right. Hand-written side projects haven’t entirely lost their value. They&#39;re just less impressive. They’re like low-intensity cycling: nobody’s excited by it, but it builds the cardiovascular base that makes the hard sessions possible. Digging through a codebase like etcd is the hard session. Without the base, AI was my e-bike. I covered the distance, but my legs didn’t do most of the work. That’s fine for getting somewhere, but not ideal for getting fitter.</p>

<hr>

<p><span id="fn1">1.</span> I’ve chosen the word senior because “expert” is hard to pin down. For instance, managers regularly judge seniority in the industry, though the technical bar for “senior” differs by org; some levelling frameworks try to formalise it. These frameworks cover much more than technical skill, but insofar as they cover the technical side, this from the senior (L50) section of <a href="https://monzo.com/documents/engineering-progression-framework-v4-1.pdf">this framework</a> is a good example: “Has the skills to proficiently work on complex and unknown problems under pressure (e.g. debugging complex production issues at speed, or leading the resolution of critical or major incidents)”. Notice it doesn’t mention writing code. This kind of skill isn&#39;t automatable yet (though with enough telemetry, an agent can help investigate production issues).</p>

<p><span id="fn2">2.</span> The first prompt, run at v0.1.0, v2.0.0, v3.0.0 and v3.7.1: “Explore this codebase and outline its public APIs, its fundamental architecture and software design choices in a Markdown document: etcd-0.1.0.md. References to source code should support all points; priority should be given to where important abstractions are declared/implemented and where they earn their place and, if relevant, where they break down.”</p>

<p><span id="fn3">3.</span> The second prompt, used to generate a summary: “Read the markdown documents in the working tree. These all outline the design and trade-offs of etcd at a particular point in time. Generate another document that traces the development of the five most important systems or abstractions over the lifetime of the project. Once one area has been chosen, the source code at the four documented versions should be double-checked and referenced. Pay attention to trade-offs, management of complexity and how abstractions break down as well as design successes.”</p>

<p><span id="fn4">4.</span> I’m not a distributed systems expert, so I’m not going to try to summarise Raft beyond saying all nodes maintain write-ahead logs from which state can be built, all writes go through a single leader, and it employs a leadership election mechanism to survive faults. For a more detailed discussion of the Raft algorithm, you can read the <a href="https://raft.github.io/raft.pdf">original paper</a>. To get some intuition of how leadership and elections work, there’s a web-based simulation <a href="https://raft.github.io/#raftscope">here</a>.</p>
]]></content:encoded>
      <guid>https://simoncrowe.blog/if-ai-writes-the-side-projects-how-do-engineers-level-up</guid>
      <pubDate>Sat, 10 Oct 2026 17:26:54 +0000</pubDate>
    </item>
    <item>
      <title>Scaling Searle&#39;s Chinese Room For Large Language Models</title>
      <link>https://simoncrowe.blog/scaling-searles-chinese-room-for-large-language-models-jtj6</link>
      <description>&lt;![CDATA[Today I&#39;m borrowing a 46-year-old philosophical idea and applying it to contemporary generative AI. I’m curious about what happens to this idea when applied to AI that, unlike what was speculated about in Searle’s time, has actually been built. &#xA;&#xA;John Searle devised his famous Chinese Room thought experiment in response to what he called strong AI. This wasn’t the idea of human-like or superhuman artificial general intelligence (AGI) tech CEOs rave about, but the idea that machines that behave as though they think truly think. One reason these ideas were entertained at the time was that the frontier of artificial intelligence was symbolic rather than numerical, meaning that systems were structured according to formal grammar and logicsup1/sup. A scholar who thinks about rules underpinning language all day may be attracted to the idea of a rule-based system that itself thinks. It’s less compelling to imagine that a concatenation of matrix multiplications and other computations understands the sequences of tokens it builds.&#xA;&#xA;I won’t go into detail about the thought experiment, though I’d encourage you to read it in a href=&#34;https://rintintin.colorado.edu/~vancecd/phil201/Searle.pdf&#34;the original article/a. The basic idea is that someone can produce convincing text-based answers to questions in a language they know nothing about without understanding the questions or answers. All they need are exhaustive formal rules in a language they do know about, on how to respond to sequences of — to them — meaningless symbols.&#xA;&#xA;Because software that lived up to Searle’s room was never actually builtsup2/sup, it benefits from an undefined scale. A single person can plausibly follow the potentially vast set of rules because no one formalised them. Due to the number of computations involved in frontier LLM inference and the limited speed at which people can perform arithmetic operations, the subject would need to live for around 380,000 years to generate a token in a conversation with a chatbot. sup3/sup Constrained by human lifetimes, the computation would need to be carried out by a deeply rigid and dogmatic civilisation organised around the purpose of ingesting and producing holy numbers. Even such a dystopia, similar in size to 19th-century England, using telegraph to communicate would produce only five tokens a year. sup4/sup&#xA;&#xA;I&#39;m not the first to scale this idea up to population size. Ned Block got there before Searle even wrote his paper: his 1978 &#34;China Brain&#34; imagined the entire population of China, linked by radio, simulating a brain neuron by neuron. Searle himself later answered the connectionists with a &#34;Chinese Gym&#34;: a hall of monolingual English speakers hand-cranking a neural network. So the move I&#39;m making here is a well-worn one. What&#39;s changed is that the machine on the other side of the analogy now actually exists, which means the scale is no longer a matter of taste: we can put numbers on it.&#xA;&#xA;Let&#39;s circle back to Searle&#39;s point about incomprehension. I&#39;ll try to sketch it out. The scribes or human computers carrying out the calculation wouldn&#39;t know what the numbers meant or what they corresponded to. Those privileged few who could access the vaults where the consecrated weight numbers and numbers for divined and generated tokens were stored would not know what symbols the numbers mapped to. The high-ranking bureaucrats who organised inference computation would blindly follow a kind of bureaucratic scripture: their command and control infrastructure operating in the spirit of the Soviet factories that, graded on tonnage, produced chandeliers heavy enough to tear down ceilings, and nail plants that met their quotas with a handful of railway-spike-sized nails. &#xA;&#xA;Scientists caught researching the inference process or building mechanical computers would be publicly executed as heretics. Those who pursued their research in secret would live lonely lives, questioning the consistency of the system they were enmeshed in but lacking a solid basis for any conclusions. Even during the great conjunctions that occur once a generation, the seers who divined new input token numbers from the entrails of sacrificial animals would not know what the numbers meant.&#xA;&#xA;So there it is: an updated Chinese Room, in all its absurdity. It works better as an illustration of just how big large language models are than as a thought experiment. Searle&#39;s room has the advantage of not being a closed system, where questions and answers can simply pass under the door. My civilisation has to smuggle its inputs in through an oracle: the divination.&#xA;&#xA;---&#xA;&#xA;span id=&#34;fn1&#34;1./span This is now known as GOFAI: good old-fashioned AI, a term coined by the philosopher John Haugeland in 1985. Think of SHRDLU, which parsed typed English commands to manipulate a virtual world of coloured blocks; MYCIN, which diagnosed blood infections by chaining through roughly 600 hand-written if-then rules; or Cyc, a decades-long attempt to hand-code common sense as millions of logical assertions. Or ELIZA, the 1966 chatbot that parodied a Rogerian psychotherapist using simple pattern-matching rules, fooling some users into feeling understood, which is precisely the illusion Searle was attacking.&#xA;&#xA;span id=&#34;fn2&#34;2./span This kind of symbolic AI Searle was talking about didn’t make it past R&amp;D, let alone being deployed commercially on a planetary scale like generative AI is today. Despite some early hype, research into expert systems and the like was eventually abandoned, leading to the second AI winter starting in the late 1980s.&#xA;&#xA;span id=&#34;fn3&#34;3./span GPT-4.5, which passed the Turing test most of the time in a a href=&#34;https://arxiv.org/abs/2503.23674&#34;2025 study/a, had hundreds of billions of parameters; OpenAI never disclosed a figure, but following a href=&#34;https://www.interconnects.ai/i/158107244/what-gpt-45-is-good-for&#34;an educated guess/a let’s say 600 billion active parameters. Assuming two computations for each parameter to generate a token, it would take an immortal, tireless human 3,169 years to complete a single layer of the transformer that drove GPT-4.5. A single token would take at least 380,000 years. This is based on 0.1 arithmetic operations per second for a human scribe.&#xA;&#xA;span id=&#34;fn4&#34;4./span A significant amount of computation that a transformer does is embarrassingly parallel: the vanilla feed-forward neural network that follows the self-attention magic. This could be parallelised across a galaxy if it weren’t for the speed of light nullifying any parallel gains with a huge communication cost. Barring a mega-structure like a Dyson sphere, the distances signals would need to travel within a solar system for each layer of a transformer would be prohibitive. Within a single small landmass, however, batches of computation can be sent by telegraph to outlying population centres very quickly. A core city of 1 million scribes working around the clock in two 12-hour shifts, 10 million more spread across the land, could generate a GPT-4.5 token in just over two months. See the diagram below for an illustration of this. See also a href=&#34;https://bbycroft.net/llm&#34;this website/a for a useful illustration of how transformers and LLMs work.&#xA;&#xA;                    O N E   T O K E N   B Y   T E L E G R A P H&#xA;&#xA;             the forward pass at city scale: a capital, its towns, a wire&#xA;&#xA;  model      600 B parameters, 120 sequential layers, dmodel 20,412&#xA;  payload    one hidden state = 20,412 values x 16 bit  ~=  327 kbit&#xA;  the wire   100 bit/s telegraph, signals at 0.7c -- the 300 km costs a&#xA;             millisecond and a half of flight and hours of keying; at this&#xA;             scale bandwidth is the tyrant, never the distance&#xA;&#xA;         &#xA;        /\       +&#xA;   |(+)|     |&#xA;  /\  | : |    /   \      +                ~ 300 km of wire ~&#xA;  | |  | : |   /     \    /\   ~        ,       ,       ,       ,          +&#xA;  | |  | : |  /       \   | |  ~        -+-------+-------+-------+-        /\&#xA; | | | : | |         |  | |          |       |       |       |   /\    | |   /\&#xA; | : | | : | |  :: ::  | | ||    |     |       |       |       |  /_\  | | /\&#xA; | : | | : | |  :: ::  | | : || [] |     |       |       |       |  |[]|  | : | |[]|&#xA;|__||__||________||||||__|_|_||||_||_|&#xA;&#xA;            C A P I T A L                     the telegraph            T O W N S&#xA;       1,000,000 core scribes         100 bit/s, signals at 0.7c   10,000,000 scribes&#xA;&#xA;-------------------------------------------------------------------------------------&#xA;                     ONE LAYER  --  k of 120, strictly in order&#xA;-------------------------------------------------------------------------------------&#xA;&#xA;  clock         CAPITAL (core)                             TOWNS (periphery)&#xA;            1e6 scribes, 10 km wide                     1e7 scribes, 300 km out&#xA;    .                  |                                           |&#xA; +0.0 h    +-----------+-----------+                               |&#xA;    .      | SELF-ATTENTION        |                               |&#xA;    .      | half the city on duty |                               |&#xA;    .      | (two 12-hour shifts)  |                               |&#xA;    .      | core only: every desk |                               |&#xA;    .      | may read the whole    |                               |&#xA;    .      | token census          |                               |&#xA;    .      |  9.4 h arithmetic     |                               |&#xA;    .      | +1.8 h keying across  |                               |&#xA;    .      |  the city&#39;s own wires |                               |&#xA;+11.3 h    +-----------+-----------+                               |&#xA;    .                  |                                           |&#xA;    .                  |          MLP inputs for layer k           |&#xA;+11.3 h                |------- 327 kbit ~ 0.9 h of keying -------  |&#xA;    .                  |       (300 km of light-lag: 1.4 ms)       |&#xA;    .                  |                                           |&#xA;+12.2 h    +-----------+-----------+                 +-------------+-------------+&#xA;    .      | MLP: capital&#39;s share  |                 | MLP FEED-FORWARD          |&#xA;    .      | the on-duty half joins|     .  .  .     | ten million desks, each   |&#xA;    .      | 0.9 h                 |     in step     | on its own slice; no town |&#xA;    .      |                       |     .  .  .     | consults another  (0.9 h) |&#xA;+13.0 h    +-----------+-----------+                 +-------------+-------------+&#xA;    .                  |                                           |&#xA;    .                  |          MLP outputs for layer k          |&#xA;+13.0 h                |&lt;------ 327 kbit ~ 0.9 h of keying --------|&#xA;    .                  |                                           |&#xA;+13.9 h    +-----------+-----------+                               |&#xA;    .      | residual add + norm   |                               |&#xA;    .      | file the layer record |                               |&#xA;    .      | (negligible)          |                               |&#xA;    .      +-----------+-----------+                               |&#xA;    .                  |                                           |&#xA;    v                  v                                           v&#xA;             hidden state to layer                       towns sit idle until&#xA;             k+1: back to the top                      the next dispatch keys in&#xA;&#xA;-------------------------------------------------------------------------------------&#xA;  THE BILL&#xA;-------------------------------------------------------------------------------------&#xA;&#xA;  one layer     9.4 + 1.8 + 0.9 + 0.9 + 0.9   =   13.9 hours&#xA;  one token     13.95 h  x  120 layers        =   69.7 days&#xA;  the split     74% labour / 26% communication&#xA;&#xA;  the capital keys and computes around the clock: two 12-hour shifts, half a&#xA;  million desks always lit. the towns need no relief -- each works 0.9 h in&#xA;  every 13.9, and the wire itself schedules their rest.&#xA;&#xA;  every communication hour is serialization -- a fist tapping 327,000 bits&#xA;  onto the wire at 100 per second. propagation never shows up on the bill:&#xA;  the light-lag budget for the whole token is about a third of a second.&#xA;&#xA;`]]&gt;</description>
      <content:encoded><![CDATA[<p>Today I&#39;m borrowing a 46-year-old philosophical idea and applying it to contemporary generative AI. I’m curious about what happens to this idea when applied to AI that, unlike what was speculated about in Searle’s time, has actually been built.</p>

<p>John Searle devised his famous Chinese Room thought experiment in response to what he called strong AI. This wasn’t the idea of human-like or superhuman artificial general intelligence (AGI) tech CEOs rave about, but the idea that machines that behave as though they think truly think. One reason these ideas were entertained at the time was that the frontier of artificial intelligence was symbolic rather than numerical, meaning that systems were structured according to formal grammar and logic<sup><a href="#fn1">1</a></sup>. A scholar who thinks about rules underpinning language all day may be attracted to the idea of a rule-based system that itself thinks. It’s less compelling to imagine that a concatenation of matrix multiplications and other computations understands the sequences of tokens it builds.</p>

<p>I won’t go into detail about the thought experiment, though I’d encourage you to read it in <a href="https://rintintin.colorado.edu/~vancecd/phil201/Searle.pdf">the original article</a>. The basic idea is that someone can produce convincing text-based answers to questions in a language they know nothing about without understanding the questions or answers. All they need are exhaustive formal rules in a language they do know about, on how to respond to sequences of — to them — meaningless symbols.</p>

<p>Because software that lived up to Searle’s room was never actually built<sup><a href="#fn2">2</a></sup>, it benefits from an undefined scale. A single person can plausibly follow the potentially vast set of rules because no one formalised them. Due to the number of computations involved in frontier LLM inference and the limited speed at which people can perform arithmetic operations, the subject would need to live for around 380,000 years to generate a token in a conversation with a chatbot. <sup><a href="#fn3">3</a></sup> Constrained by human lifetimes, the computation would need to be carried out by a deeply rigid and dogmatic civilisation organised around the purpose of ingesting and producing <em>holy numbers</em>. Even such a dystopia, similar in size to 19th-century England, using telegraph to communicate would produce only five tokens a year. <sup><a href="#fn4">4</a></sup></p>

<p>I&#39;m not the first to scale this idea up to population size. Ned Block got there before Searle even wrote his paper: his 1978 “China Brain” imagined the entire population of China, linked by radio, simulating a brain neuron by neuron. Searle himself later answered the connectionists with a “Chinese Gym”: a hall of monolingual English speakers hand-cranking a neural network. So the move I&#39;m making here is a well-worn one. What&#39;s changed is that the machine on the other side of the analogy now actually exists, which means the scale is no longer a matter of taste: we can put numbers on it.</p>

<p>Let&#39;s circle back to Searle&#39;s point about incomprehension. I&#39;ll try to sketch it out. The scribes or human <em>computers</em> carrying out the calculation wouldn&#39;t know what the numbers meant or what they corresponded to. Those privileged few who could access the vaults where the consecrated weight numbers and numbers for divined and generated tokens were stored would not know what symbols the numbers mapped to. The high-ranking bureaucrats who organised inference computation would blindly follow a kind of bureaucratic scripture: their command and control infrastructure operating in the spirit of the Soviet factories that, graded on tonnage, produced chandeliers heavy enough to tear down ceilings, and nail plants that met their quotas with a handful of railway-spike-sized nails.</p>

<p>Scientists caught researching the inference process or building mechanical computers would be publicly executed as heretics. Those who pursued their research in secret would live lonely lives, questioning the consistency of the system they were enmeshed in but lacking a solid basis for any conclusions. Even during the great conjunctions that occur once a generation, the seers who divined new input token numbers from the entrails of sacrificial animals would not know what the numbers meant.</p>

<p>So there it is: an updated Chinese Room, in all its absurdity. It works better as an illustration of just how big large language models are than as a thought experiment. Searle&#39;s room has the advantage of not being a closed system, where questions and answers can simply pass under the door. My civilisation has to smuggle its inputs in through an oracle: the divination.</p>

<hr>

<p><span id="fn1">1.</span> This is now known as GOFAI: good old-fashioned AI, a term coined by the philosopher John Haugeland in 1985. Think of SHRDLU, which parsed typed English commands to manipulate a virtual world of coloured blocks; MYCIN, which diagnosed blood infections by chaining through roughly 600 hand-written if-then rules; or Cyc, a decades-long attempt to hand-code common sense as millions of logical assertions. Or ELIZA, the 1966 chatbot that parodied a Rogerian psychotherapist using simple pattern-matching rules, fooling some users into feeling understood, which is precisely the illusion Searle was attacking.</p>

<p><span id="fn2">2.</span> This kind of symbolic AI Searle was talking about didn’t make it past R&amp;D, let alone being deployed commercially on a planetary scale like generative AI is today. Despite some early hype, research into expert systems and the like was eventually abandoned, leading to the second AI winter starting in the late 1980s.</p>

<p><span id="fn3">3.</span> GPT-4.5, which passed the Turing test most of the time in a <a href="https://arxiv.org/abs/2503.23674">2025 study</a>, had hundreds of billions of parameters; OpenAI never disclosed a figure, but following <a href="https://www.interconnects.ai/i/158107244/what-gpt-45-is-good-for">an educated guess</a> let’s say 600 billion active parameters. Assuming two computations for each parameter to generate a token, it would take an immortal, tireless human 3,169 years to complete a single layer of the transformer that drove GPT-4.5. A single token would take at least 380,000 years. This is based on 0.1 arithmetic operations per second for a human scribe.</p>

<p><span id="fn4">4.</span> A significant amount of computation that a transformer does is embarrassingly parallel: the vanilla feed-forward neural network that follows the self-attention magic. This could be parallelised across a galaxy if it weren’t for the speed of light nullifying any parallel gains with a huge communication cost. Barring a mega-structure like a Dyson sphere, the distances signals would need to travel within a solar system for each layer of a transformer would be prohibitive. Within a single small landmass, however, batches of computation can be sent by telegraph to outlying population centres very quickly. A core city of 1 million scribes working around the clock in two 12-hour shifts, 10 million more spread across the land, could generate a GPT-4.5 token in just over two months. See the diagram below for an illustration of this. See also <a href="https://bbycroft.net/llm">this website</a> for a useful illustration of how transformers and LLMs work.</p>

<pre><code>=====================================================================================

                    O N E   T O K E N   B Y   T E L E G R A P H

             the forward pass at city scale: a capital, its towns, a wire

=====================================================================================

  model      600 B parameters, 120 sequential layers, d_model 20,412
  payload    one hidden state = 20,412 values x 16 bit  ~=  327 kbit
  the wire   100 bit/s telegraph, signals at 0.7c -- the 300 km costs a
             millisecond and a half of flight and hours of keying; at this
             scale bandwidth is the tyrant, never the distance


         _
        /_\       +
   +   |(+)|     _|_
  /_\  | : |    /   \      +                ~ 300 km of wire ~
  | |  | : |   /     \    /_\   ~        ,       ,       ,       ,          +
  | |  | : |  /       \   | |  ~        -+-------+-------+-------+-        /_\
 _| |_ | : | |         |  | |   _  _     |       |       |       |   /\    | |   /\
 | : | | : | |  :: ::  | _| |_|    |     |       |       |       |  /__\  _| |_ /__\
 | : | | : | |  :: ::  | | : || [] |     |       |       |       |  |[]|  | : | |[]|
_|___|_|___|_|_________|_|___||____|_____|_______|_______|_______|__|__|__|___|_|__|_

            C A P I T A L                     the telegraph            T O W N S
       1,000,000 core scribes         100 bit/s, signals at 0.7c   10,000,000 scribes


-------------------------------------------------------------------------------------
                     ONE LAYER  --  k of 120, strictly in order
-------------------------------------------------------------------------------------

  clock         CAPITAL (core)                             TOWNS (periphery)
            1e6 scribes, 10 km wide                     1e7 scribes, 300 km out
    .                  |                                           |
 +0.0 h    +-----------+-----------+                               |
    .      | SELF-ATTENTION        |                               |
    .      | half the city on duty |                               |
    .      | (two 12-hour shifts)  |                               |
    .      | core only: every desk |                               |
    .      | may read the whole    |                               |
    .      | token census          |                               |
    .      |  9.4 h arithmetic     |                               |
    .      | +1.8 h keying across  |                               |
    .      |  the city&#39;s own wires |                               |
+11.3 h    +-----------+-----------+                               |
    .                  |                                           |
    .                  |          MLP inputs for layer k           |
+11.3 h                |------- 327 kbit ~ 0.9 h of keying -------&gt;|
    .                  |       (300 km of light-lag: 1.4 ms)       |
    .                  |                                           |
+12.2 h    +-----------+-----------+                 +-------------+-------------+
    .      | MLP: capital&#39;s share  |                 | MLP FEED-FORWARD          |
    .      | the on-duty half joins|     .  .  .     | ten million desks, each   |
    .      | 0.9 h                 |     in step     | on its own slice; no town |
    .      |                       |     .  .  .     | consults another  (0.9 h) |
+13.0 h    +-----------+-----------+                 +-------------+-------------+
    .                  |                                           |
    .                  |          MLP outputs for layer k          |
+13.0 h                |&lt;------ 327 kbit ~ 0.9 h of keying --------|
    .                  |                                           |
+13.9 h    +-----------+-----------+                               |
    .      | residual add + norm   |                               |
    .      | file the layer record |                               |
    .      | (negligible)          |                               |
    .      +-----------+-----------+                               |
    .                  |                                           |
    v                  v                                           v
             hidden state to layer                       towns sit idle until
             k+1: back to the top                      the next dispatch keys in


-------------------------------------------------------------------------------------
  THE BILL
-------------------------------------------------------------------------------------

  one layer     9.4 + 1.8 + 0.9 + 0.9 + 0.9   =   13.9 hours
  one token     13.95 h  x  120 layers        =   69.7 days
  the split     74% labour / 26% communication

  the capital keys and computes around the clock: two 12-hour shifts, half a
  million desks always lit. the towns need no relief -- each works 0.9 h in
  every 13.9, and the wire itself schedules their rest.

  every communication hour is serialization -- a fist tapping 327,000 bits
  onto the wire at 100 per second. propagation never shows up on the bill:
  the light-lag budget for the whole token is about a third of a second.

=====================================================================================

</code></pre>
]]></content:encoded>
      <guid>https://simoncrowe.blog/scaling-searles-chinese-room-for-large-language-models-jtj6</guid>
      <pubDate>Sun, 05 Jul 2026 16:20:32 +0000</pubDate>
    </item>
    <item>
      <title>Abiogenis: a New Project</title>
      <link>https://simoncrowe.blog/abiogenis-a-new-project</link>
      <description>&lt;![CDATA[In 2025, I spent a lot of time on a project to help me find a flat using a cloud-hosted LLM. This was interesting enough and helped me with my CKAD exam, but for 2026, I’ve decided to do something that scratches a more philosophical itch.&#xA;&#xA;Abiogenis is a theorised process whereby nonliving matter develops into prototypical forms of living matter: simple self-replicating structures, RNA, proto-cells. I find this transition from chemistry to biology very interesting, because:&#xA;&#xA;It could explain the origins of life on Earth (and elsewhere)&#xA;It should be possible to simulate without resorting to extremely simplified models, like rigid body physics for animals.&#xA;The molecular/cellular scale can have an otherworldly aesthetic, which may work with the generative music part of the project.&#xA;&#xA;What am I planning to build?&#xA;&#xA;Here are the rough ideas as they stand: I want to build some web-based software that allows users to navigate space and time in a primordial soup. Time may be folded into space, and then the four dimensions (instead of the usual three) projected onto two. Instead of exerting agency, the viewer will be a passive observer. Generative music, the sophistication of which follows that of the simulated molecules, will be a big part of the viewer’s experience.&#xA;&#xA;I plan to use WebGL and WebAssembly in a relatively low-level manner to handle potentially complex 3D scenes and heavy computation for the state. I’ll probably use Rust, as this is the only low-level language I’ve built anything with.  Rust as a technical choice may fall over because I also intend to use as much AI tooling as possible. An older and simpler language like C is likely easier on LLMs and to have greater representation in the training data. I’ll adapt to the constraints of the tools. I’ll mainly rely on AI to augment my basic mathematical knowledge and utilize my experience in building software to ensure that the code is structured sanely. I&#39;ll likely write the music-related code in JavaScript and use something like SuperSonic as a synthesis engine; I&#39;m imagining a few subtractive voices and lots of reverb.&#xA;&#xA;While I could have chosen a less ambitious project to test AI coding tools, I feel that enough of my time is spent being pragmatic in my professional life. I don’t want to use my limited spare time to create a React app, Django HTTP API, or some Go microservices.&#xA;&#xA;Why?&#xA;&#xA;I haven’t read much philosophy since attempting a journal article on AI and the machinic unconscious over six years ago. I’ve been busy and have only read humanities books that strongly piqued my interest. One of these was a review of consciousness in recent analytical philosophy. I found the clear communication and modest claims of analytical philosophy refreshing, especially after doing two arts degrees where continental thought is far more popular (with notable exceptions). At worst, continental and continental-inspired writing is a sequence of complex sentences telling the reader how a constellation of different abstract concepts relate to each-other with no definitions or reference to facts. However, there are trade-offs: analytical philosophy can be dull. Much of the book could be summed up as follows: The hard problem of consciousness is indeed hard. There is overwhelming evidence that mental states correlate with brain states; we can’t agree on the details. Let’s find a palatable way of representing the situation that best fits our everyday experience of embodied rational agency. Here is a menagerie of -isms and some thought experiments.&#xA;&#xA;If I wanted satisfaction, I should have read a book about a problem that can be solved. This brings me to abiogenesis. While no one has produced biology from non-living chemicals in a lab, there are a lot of models and theories for how it might work, and more on how primitive life works today. Attempting to make a computer simulation of how consciousness functions in relation to brain activity would be impossible. Where would you start? Large language models have passed the Turing test, but like all machine learning models, we have no reason to believe they’re any more conscious than a wheel of Camembert.  We could set about tracing information as it flows through systems that populate a person’s mind; it might be interesting social science, but it wouldn’t address the hard problem of consciousness. Chemical and biological processes are more compatible than physical and mental substances, properties, whatever.&#xA;&#xA;The final thing I’d like to share is a passage from a book that hit the sweet spot between the analytical and continental: Manuel de Landa’s A Thousand Years of Nonlinear History. He adapts and applies some of the most notoriously impenetrable continental ideas from Deleuze and Guattari in a concrete, historically grounded way. The book talks about the material flows that drive history through geology, biology, and language. I remembered a particular passage when thinking about abiogenesis:&#xA;&#xA;  […]  positive feedback links between these flows began to form closed-circuits: antimarket money flowed into mining regions and intensified coal production and iron extraction, which triggered a flow of mechanical energy (steam), which in turn triggered a flow of cotton textiles, which created the flow of profits that financed further experimentation with coal, iron and steam technology. These loops of triggers and flows were behind the explosive urban growth in England between 1750 and 1850. […] And precisely these autocatalytic loops were what kept this self-organised structure going.&#34;&#xA;&#xA;Autocatalysis is a likely step in the path from non-living to living matter. I find the idea that this kind of process also emerges at the macro level quite inspiring.]]&gt;</description>
      <content:encoded><![CDATA[<p>In 2025, I spent a lot of time on <a href="https://simoncrowe.hashnode.dev/building-a-flat-hunting-shortlist-with-ai-and-a-polyglot-cloud-backend">a project</a> to help me find a flat using a cloud-hosted LLM. This was interesting enough and helped me with my <a href="https://training.linuxfoundation.org/certification/certified-kubernetes-application-developer-ckad/">CKAD</a> exam, but for 2026, I’ve decided to do something that scratches a more philosophical itch.</p>

<p>Abiogenis is a theorised process whereby nonliving matter develops into prototypical forms of living matter: simple self-replicating structures, RNA, proto-cells. I find this transition from chemistry to biology very interesting, because:</p>
<ul><li>It could explain the origins of life on Earth (and elsewhere)</li>
<li>It should be possible to simulate without resorting to extremely simplified models, like <a href="https://www.youtube.com/watch?v=oFqwcngsts8">rigid body physics for animals</a>.</li>
<li>The molecular/cellular scale can have an otherworldly aesthetic, which may work with the generative music part of the project.</li></ul>

<h2 id="what-am-i-planning-to-build">What am I planning to build?</h2>

<p>Here are the rough ideas as they stand: I want to build some web-based software that allows users to navigate space and time in a primordial soup. Time may be folded into space, and then the four dimensions (instead of the usual three) projected onto two. Instead of exerting agency, the viewer will be a passive observer. Generative music, the sophistication of which follows that of the simulated molecules, will be a big part of the viewer’s experience.</p>

<p>I plan to use WebGL and WebAssembly in a relatively low-level manner to handle potentially complex 3D scenes and heavy computation for the state. I’ll probably use Rust, as this is the only low-level language I’ve built anything with.  Rust as a technical choice may fall over because I also intend to use as much AI tooling as possible. An older and simpler language like C is likely easier on LLMs and to have greater representation in the training data. I’ll adapt to the constraints of the tools. I’ll mainly rely on AI to augment my basic mathematical knowledge and utilize my experience in building software to ensure that the code is structured sanely. I&#39;ll likely write the music-related code in JavaScript and use something like <a href="https://sonic-pi.net/supersonic/demo.html">SuperSonic</a> as a synthesis engine; I&#39;m imagining a few subtractive voices and <strong>lots</strong> of reverb.</p>

<p>While I could have chosen a less ambitious project to test AI coding tools, I feel that enough of my time is spent being pragmatic in my professional life. I don’t want to use my limited spare time to create a React app, Django HTTP API, or some Go microservices.</p>

<h2 id="why">Why?</h2>

<p>I haven’t read much philosophy since attempting a <a href="https://spheres-journal.org/contribution/micropolitics-of-a-recommender-system-machine-learning-and-the-machinic-unconscious/">journal article</a> on AI and the machinic unconscious over six years ago. I’ve been busy and have only read humanities books that strongly piqued my interest. One of these was a <a href="https://www.goodreads.com/book/show/32486030-consciousness">review of consciousness in recent analytical philosophy</a>. I found the clear communication and modest claims of analytical philosophy refreshing, especially after doing two arts degrees where continental thought is far more popular (with <a href="https://monoskop.org/images/4/46/Kosuth_Joseph_1969_1991_Art_After_Philosophy.pdf">notable exceptions</a>). At worst, continental and continental-inspired writing is a sequence of complex sentences telling the reader how a constellation of different abstract concepts relate to each-other with no definitions or reference to facts. However, there are trade-offs: analytical philosophy can be dull. Much of the book could be summed up as follows: <em>The hard problem of consciousness is indeed hard. There is overwhelming evidence that mental states correlate with brain states; we can’t agree on the details. Let’s find a palatable way of representing the situation that best fits our everyday experience of embodied rational agency. Here is a menagerie of -isms and some thought experiments.</em></p>

<p>If I wanted satisfaction, I should have read a book about a problem that can be solved. This brings me to abiogenesis. While no one has produced biology from non-living chemicals in a lab, there are a lot of models and theories for how it might work, and more on how primitive life works today. Attempting to make a computer simulation of how consciousness functions in relation to brain activity would be impossible. Where would you start? Large language models have passed the Turing test, but like all machine learning models, we have no reason to believe they’re any more conscious than a wheel of Camembert.  We could set about tracing information as it flows through systems that populate a person’s mind; it might be interesting social science, but it wouldn’t address the hard problem of consciousness. Chemical and biological processes are more compatible than physical and mental <em>substances</em>, <em>properties</em>, whatever.</p>

<p>The final thing I’d like to share is a passage from a book that hit the sweet spot between the analytical and continental: Manuel de Landa’s A Thousand Years of Nonlinear History. He adapts and applies some of the most notoriously impenetrable continental ideas from Deleuze and Guattari in a concrete, historically grounded way. The book talks about the material flows that drive history through geology, biology, and language. I remembered a particular passage when thinking about abiogenesis:</p>

<blockquote><p>[…]  positive feedback links between these flows began to form closed-circuits: antimarket money flowed into mining regions and intensified coal production and iron extraction, which triggered a flow of mechanical energy (steam), which in turn triggered a flow of cotton textiles, which created the flow of profits that financed further experimentation with coal, iron and steam technology. These loops of triggers and flows were behind the explosive urban growth in England between 1750 and 1850. […] And precisely these autocatalytic loops were what kept this self-organised structure going.”</p></blockquote>

<p>Autocatalysis is a likely step in the path from non-living to living matter. I find the idea that this kind of process also emerges at the macro level quite inspiring.</p>
]]></content:encoded>
      <guid>https://simoncrowe.blog/abiogenis-a-new-project</guid>
      <pubDate>Sun, 28 Dec 2025 14:26:20 +0000</pubDate>
    </item>
    <item>
      <title>I deployed this WriteFreely Blog on a Cheap Hetzner VM using OpenTofu (IaC)</title>
      <link>https://simoncrowe.blog/i-deployed-this-writefreely-blog-on-a-cheap-hetzner-vm-using-opentofu-iac</link>
      <description>&lt;![CDATA[Last year, I created an engineering-focused blog on hashnode.dev. It was easy, and it served its purpose. However, as time went on, I developed a few niggles with the blogging platform -- mainly these:&#xA;&#xA;Writers are at the mercy of the business, which can start charging for things and generally enshitifiying the platform at any time. They could paywall writers&#39; content, like Medium does.&#xA;My blog was associated with the general reputation of content on the platform, which wasn&#39;t always favourable.&#xA;I don&#39;t only want to write about software engineering, and that is the focus of the platform.&#xA;&#xA;My reasons for choosing a self-hosted WriteFreely are essentially the inverse of the above points, with a couple more:&#xA;&#xA;Writefreely has an ActivityPub integration, so my blog can be a true citizen of the Fediverse.&#xA;WriteFreely is very simple. It&#39;s a Go monolith that can (and in the case of this site, does) run with a SQLite database. It uses Markdown, so no fancy text-editing widgets.&#xA;&#xA;How did I do it?&#xA;Now that I&#39;ve outlined why, I can move on to the unashamedly geeky part of this post. My main objectives were to run Writefreely in a way that was both simple and cheap. &#xA;&#xA;Simplicity&#xA;I got a lot of simplicity for free with this WriteFreely Docker image, which neatly encapsulates the initial configuration of WriteFreely as well as data migrations. The image doesn&#39;t leverage Writefreely&#39;s built-in automatic HTTPS. Luckily, another Go-based project, Caddy, does this, on top of being a solid reverse proxy. &#xA;&#xA;Those of you who have used the likes of Apache or NGINX, but not Caddy, please take a moment to admire the two lines of configuration necessary to reverse proxy this blog:&#xA;simoncrowe.blog&#xA;&#xA;reverseproxy localhost:8080&#xA;Yes, this is all that&#39;s required to serve this blog and automatically provision TLS certificates using Let&#39;s Encrypt!&#xA;&#xA;Frugality&#xA;The localhost origin in my Cadddyfile above isn&#39;t typical of a Docker-compose setup. It&#39;s a result of my trying to save money by not renting a public IPv4 address from Hetzner and only using a free IPv6 address. Caddy needed an internet-routable IPv6 address to get responses from Let&#39;s Encrypt&#39;s servers, and using the &#34;host&#34; network mode was the simplest way to achieve this.&#xA;&#xA;The laptop I&#39;m typing this on doesn&#39;t have an IPv6 address, so I couldn&#39;t SSH into the server, even if I wanted to. To get around this for HTTPS traffic, I used Cloudflare as a DNS registry and proxy. This means that simoncrowe.blog resolves to some of CloudFlare&#39;s v4 IPs. CloudFlare handles everything between the Hetzner IPv6 origin and its own dual-stack infrastructure, at no cost.&#xA;&#xA;Trade-off time: My insistence on not paying the tiny IPv4 address fee has probably increased the complexity of this. The no SSH constraint is what motivated me to write a Terraform/OpenTofu module. Using the Hetzner web shell to configure even a simple setup like this would have been a miserable experience compared to SSH from the comfort of my laptop&#39;s terminal emulator. Because I&#39;ve been lazy and put the database in the same machine as the HTTP server, I&#39;ve already consigned the server to a long existence as a pet. Immutable Infrastructure is not happening here, so doing everything via the Hetzner web console and SSH would have been reasonable.&#xA;&#xA;On the other hand, maybe my code will help someone else run their own WriteFreely. Here&#39;s the repo.&#xA;&#xA;Cheap compute with a cloud API! What else can Hetzner do?&#xA;Since I managed to throw something functional together in a weekend, I&#39;m wondering what one could do with Hetzner and IaC tooling like OpenTofu, given some time.&#xA;&#xA;While I haven&#39;t explored their full networking and compute capabilities, the impression I got is that they&#39;d be fine to run an early stage European startup&#39;s backend (with some caveats). Anything approaching a scale-up with global ambitions would be better off with a major public cloud provider. The main reason I say this is that they only have data centres in Germany, Finland, the US and Singapore (the latter two being rack-space in 3rd party data centres).&#xA;&#xA;To me, Hetzner feels somewhere between a typical shared LAMP stack web hosting provider one might use for a Joomla/WordPress site and a mail server -- and rudimentary AWS (mainly ec2, some s3 and recently a bit of route53). When creating VMs manually on Hetzner, you can install one piece of common software. Like the LAMP hosting, options include WordPress, but also more advanced things like Docker and Prometheus/Grafana. Unlike typical web hosting, you can provision infrastructure using an API, and you get root access to your servers. Rather quaintly, when a server spins up, you get an email with the default root password (luckily, sshd isn&#39;t running out of the box).&#xA;&#xA;If I were a frugal European startup founder, the only thing that would stop me from choosing Hetzner is the lack of managed databases. The VMs could easily run stateless workloads where new software versions are rolled out smoothly using OpenTofu lifecycle policies. While one could run a database on such a VM, one would need to be very careful about backups and learn database administration. A tolerable middle ground might be running production databases using AWS RDS/Aurora in AWS regions geographically close to Hetzner locations; sure, there would be some extra latency, but it might be manageable.&#xA;&#xA;I&#39;ll keep an eye on Hetzner. I&#39;m impressed with how straightforward everything is with them. At the top of my wish list would be managed stateful services like Postgres and Kubernetes, a container registry, and guarantees around availability zones (high availability in general). Serverless_ versions of every imaginable thing, I can live without.]]&gt;</description>
      <content:encoded><![CDATA[<p>Last year, I created an <a href="https://simoncrowe.hashnode.dev/">engineering-focused blog on hashnode.dev</a>. It was easy, and it served its purpose. However, as time went on, I developed a few niggles with the blogging platform — mainly these:</p>
<ul><li>Writers are at the mercy of the business, which can start charging for things and generally <em>enshitifiying</em> the platform at any time. They could paywall writers&#39; content, like Medium does.</li>
<li>My blog was associated with the general reputation of content on the platform, which wasn&#39;t always favourable.</li>
<li>I don&#39;t <em>only</em> want to write about software engineering, and that is the focus of the platform.</li></ul>

<p>My reasons for choosing a self-hosted WriteFreely are essentially the inverse of the above points, with a couple more:</p>
<ul><li>Writefreely has an ActivityPub integration, so my blog can be a true citizen of the Fediverse.</li>
<li>WriteFreely is very simple. It&#39;s a Go monolith that can (and in the case of this site, does) run with a SQLite database. It uses Markdown, so no fancy text-editing widgets.</li></ul>

<h2 id="how-did-i-do-it">How did I do it?</h2>

<p>Now that I&#39;ve outlined <em>why</em>, I can move on to the unashamedly geeky part of this post. My main objectives were to run Writefreely in a way that was both simple and cheap.</p>

<h3 id="simplicity">Simplicity</h3>

<p>I got a lot of simplicity for free with <a href="https://git.madhouse-project.org/algernon/writefreely-docker/src/branch/main">this WriteFreely Docker image</a>, which neatly encapsulates the initial configuration of WriteFreely as well as data migrations. The image doesn&#39;t leverage Writefreely&#39;s built-in automatic HTTPS. Luckily, another Go-based project, <a href="https://caddyserver.com/">Caddy</a>, does this, on top of being a solid reverse proxy.</p>

<p>Those of you who have used the likes of Apache or NGINX, but not Caddy, please take a moment to admire the two lines of configuration necessary to reverse proxy this blog:</p>

<pre><code>simoncrowe.blog

reverse_proxy localhost:8080
</code></pre>

<p>Yes, this is all that&#39;s required to serve this blog and automatically provision TLS certificates using Let&#39;s Encrypt!</p>

<h3 id="frugality">Frugality</h3>

<p>The <code>localhost</code> origin in my <code>Cadddyfile</code> above isn&#39;t typical of a Docker-compose setup. It&#39;s a result of my trying to save money by not renting a public IPv4 address from Hetzner and only using a free IPv6 address. Caddy needed an internet-routable IPv6 address to get responses from Let&#39;s Encrypt&#39;s servers, and using the “host” network mode was the simplest way to achieve this.</p>

<p>The laptop I&#39;m typing this on doesn&#39;t have an IPv6 address, so I couldn&#39;t SSH into the server, even if I wanted to. To get around this for HTTPS traffic, I used Cloudflare as a DNS registry and proxy. This means that <code>simoncrowe.blog</code> resolves to some of CloudFlare&#39;s v4 IPs. CloudFlare handles everything between the Hetzner IPv6 origin and its own dual-stack infrastructure, at no cost.</p>

<p>Trade-off time: My insistence on not paying the tiny IPv4 address fee has probably increased the complexity of this. The <em>no SSH</em> constraint is what motivated me to write a Terraform/OpenTofu module. Using the Hetzner web shell to configure even a simple setup like this would have been a miserable experience compared to SSH from the comfort of my laptop&#39;s terminal emulator. Because I&#39;ve been lazy and put the database in the same machine as the HTTP server, I&#39;ve already consigned the server to a long existence as a pet. <em>Immutable Infrastructure</em> is not happening here, so doing everything via the Hetzner web console and SSH would have been reasonable.</p>

<p>On the other hand, maybe my code will help someone else run their own WriteFreely. Here&#39;s <a href="https://github.com/simoncrowe/terraform-writefreely-hetzner">the repo</a>.</p>

<h2 id="cheap-compute-with-a-cloud-api-what-else-can-hetzner-do">Cheap compute with a cloud API! What else can Hetzner do?</h2>

<p>Since I managed to throw something functional together in a weekend, I&#39;m wondering what one could do with Hetzner and IaC tooling like OpenTofu, given some time.</p>

<p>While I haven&#39;t explored their full networking and compute capabilities, the impression I got is that they&#39;d be fine to run an early stage European startup&#39;s backend (with some caveats). Anything approaching a scale-up with global ambitions would be better off with a major public cloud provider. The main reason I say this is that they only have <a href="https://docs.hetzner.com/cloud/general/locations/">data centres</a> in Germany, Finland, the US and Singapore (the latter two being rack-space in 3rd party data centres).</p>

<p>To me, Hetzner feels somewhere between a typical shared LAMP stack web hosting provider one might use for a Joomla/WordPress site and a mail server — and rudimentary AWS (mainly <code>ec2</code>, some <code>s3</code> and recently a bit of <code>route53</code>). When creating VMs manually on Hetzner, you can install one piece of common software. Like the LAMP hosting, options include WordPress, but also more advanced things like Docker and Prometheus/Grafana. Unlike typical web hosting, you can provision infrastructure using an API, and you get root access to your servers. Rather quaintly, when a server spins up, you get an email with the default root password (luckily, <code>sshd</code> isn&#39;t running <em>out of the box</em>).</p>

<p>If I were a frugal European startup founder, the only thing that would stop me from choosing Hetzner is the lack of managed databases. The VMs could easily run stateless workloads where new software versions are rolled out smoothly using OpenTofu <a href="https://opentofu.org/docs/language/meta-arguments/lifecycle/">lifecycle policies</a>. While one could run a database on such a VM, one would need to be very careful about backups and learn database administration. A tolerable middle ground might be running production databases using AWS RDS/Aurora in AWS regions geographically close to Hetzner locations; sure, there would be <em>some</em> extra latency, but it might be manageable.</p>

<p>I&#39;ll keep an eye on Hetzner. I&#39;m impressed with how straightforward everything is with them. At the top of my wish list would be managed stateful services like Postgres and Kubernetes, a container registry, and guarantees around availability zones (high availability in general). <em>Serverless</em> versions of every imaginable thing, I can live without.</p>
]]></content:encoded>
      <guid>https://simoncrowe.blog/i-deployed-this-writefreely-blog-on-a-cheap-hetzner-vm-using-opentofu-iac</guid>
      <pubDate>Sat, 06 Dec 2025 23:57:04 +0000</pubDate>
    </item>
  </channel>
</rss>