Benchmarking Claude Opus 5 on Autonomous Distributed Infrastructure: Building PostgreSQL & MySQL HA Clusters from Scratch

An empirical, end-to-end evaluation of Claude Opus 5 across distributed systems design, infrastructure as code, deterministic testing, and live cloud deployment.

Executive Summary

Can a frontier model architect, implement, test, SHA-pin, and converge a production-shaped distributed system from an empty directory, then prove it survives losing a machine?

We ran Claude Opus 5 through a creation-effort benchmark inside Colors, using two cold concurrent subagents — one per database engine — with no shared memory and no cross-visibility.

  • Both arms converged on real hardware: six DigitalOcean droplets, Cloudflare DNS, and Cloudflare R2, from a pushed git SHA rather than a local working tree.
  • 17,787 lines across 199 files: Clojure packages, OpenTofu templates, Ansible playbooks, systemd units, and golden fixtures.
  • Failover exercised for real, not simulated: a PostgreSQL leader powered off at the cloud API promoted in ~9s with no data loss; a SIGKILLed MySQL primary had the endpoint writable again at T+43s.
  • Neither package reused ONCE. Both agents independently declined the established compute layer and wrote their own provider templates — within 6% of each other in size.

1. Experimental Methodology & Controls

Most coding benchmarks score a model on a self-contained function. Infrastructure work is a different problem: it has state, ordering, partial failure, credentials, and a definition of done that only a running machine can confirm. The interesting question is not whether a model can write Terraform — it is whether the thing it writes converges, and whether it survives a node dying.

Eliminating the order effect

Building one engine first teaches patterns that make the second cheaper. That transfer is larger than the difference between the engines, so a sequential A-then-B run mostly measures "second is easier." Three controls removed it:

  • Two cold concurrent subagents. Both launched in the same instant with no shared context. The MySQL agent was forbidden from reading postgres-ha*; the PostgreSQL agent was forbidden from reading mysql-ha*. Neither could see the other's design.
  • One prompt template. Both prompts were rendered from a single canonical file with an eight-row substitution table covering only engine-specific nouns — hostnames, bucket names, credential names. No example, hint, or named tool appears in one that is absent from the other.
  • Pre-written symmetric desired state. Each colors.yml started at 102 lines fixing provider, region, instance size, node budget, storage targets and the destroy guard. A diff confirmed the two files differed only in name, host, zone and bucket.

Deliberately not fixed: the replication topology, the failover orchestrator, the client endpoint mechanism, the backup tool, and the PITR mechanism. Choosing those is the work being measured, and pre-specifying them would have deleted the finding.

2. Architectural Divergence: How Claude Opus 5 Solved Each Stack

Given identical budgets — three droplets maximum, s-2vcpu-4gb, ubuntu-24-04-x64, DigitalOcean ams3, VPC discovered at runtime — the two agents produced engine-native architectures that disagree in one specific and instructive place.

PostgreSQL HAPatroni 4.1.5 · etcd 3.5.33 colocated · pgBackRest 2.59pg-ha → three A recordsfailover writes no DNS and calls no cloud APInode 1 · leaderHAProxy 2.8PatroniPostgreSQL 17etcd membersync commit ANY 1node 2 · standbyHAProxy 2.8PatroniPostgreSQL 17etcd membernode 3 · standbyHAProxy 2.8PatroniPostgreSQL 17etcd memberWAL + full backupsCloudflare R2 · postgres-ha-backupdaily full + continuous WAL + daily verified restoreLeader powered off at the API →promoted in ~9s, zero data loss, no DNS writeMySQL HAGroup Replication · reserved IP · binlog PITRmy-ha → one reserved IPthe DNS record never changes; the cluster moves the IPclaimed by PRIMARYnode 1 · primarymysqld 8.0Group Replicationbinlog spoolerPaxos, no external DCSnode 2 · secondarymysqld 8.0Group Replicationbinlog spoolernode 3 · secondarymysqld 8.0Group Replicationbinlog spoolerbinlogs every minuteCloudflare R2 · mysql-ha-backupdaily dump + continuous binlog + daily verified restorePrimary SIGKILLed →new primary at 23s, endpoint writable at 43s
Figure 1: The two topologies. Both keep quorum on three machines; they disagree about where the client endpoint lives.

PostgreSQL arm (postgres-ha & postgres-ha-digitalocean)

  • Quorum & consensus: a 3-member etcd 3.5.33 cluster colocated on the database nodes, pinned by release tag and by the SHA-256 of its tarball, driving Patroni 4.1.5 with quorum synchronous commit (ANY 1). Patroni 4 removed the raft DCS, so an external store was not optional.
  • Client routing: HAProxy 2.8 on all three nodes, port 5432 to the leader and 5433 to replicas, health-checked against Patroni's REST API, behind three Cloudflare A records. A failover changes nothing in DNS and calls no cloud API.
  • Backup & PITR: pgBackRest 2.59 to R2, with archive_command set in Patroni's DCS rather than on a node — so a promoted standby keeps archiving. WAL-G was evaluated and rejected for lacking check and alternate-path restore.
  • Verified restore: restore the newest backup, replay every archived segment, start it on a spare port, and require a leader-written heartbeat newer than 900s. A recovery_target=immediate drill would pass with archiving completely broken.

MySQL arm (mysql-ha & mysql-ha-digitalocean)

  • Quorum & consensus: native MySQL 8.0 Group Replication in single-primary mode. The three mysqld processes are the Paxos group, so the quorum store is colocated by construction and there is no orchestrator — detection, quorum and election are one mechanism instead of three that must agree.
  • Client routing: a DigitalOcean reserved IP claimed by whichever member reports PRIMARY, polled on a 10s timer. The Cloudflare record's content never changes, so OpenTofu keeps owning DNS while the cluster owns the assignment.
  • Backup & PITR: mysqldump --single-transaction --set-gtid-purged=ON with zstd, plus mysqlbinlog --read-from-remote-server --stop-never --raw running on every member and uploading each minute. Because every ONLINE member logs every transaction under the same GTID, any one prefix is a complete source and the archiver needs no leader election. XtraBackup was rejected for coupling to the distribution's MySQL build.
  • Verified restore: a scratch mysqld daily, asserting replayed_beats > 0 — the only check that distinguishes a restore from a point-in-time restore.

The disagreement worth reading twice

Each arm explicitly rejected what the other chose. The MySQL agent took a reserved IP to get one stable address, accepting a cloud API call in the failover path. The PostgreSQL agent rejected exactly that design on the grounds that it "puts a DO token on every DB node and needs an API call while degraded," and paid for the alternative in client-side complexity instead.

Neither is wrong. They weighted dependency during degradation against client simplicity differently, and both wrote down why.

3. Objective Metrics & Scorecard

Recovered from the filesystem, git history, and live cloud probes by one script run identically against both packages — so no number below is an artefact of how the two arms were counted.

Category / MetricPostgreSQL armMySQL armCombined
Convergence status✔ Converged✔ Converged2 / 2
Package size108 files (8,800 lines)91 files (8,987 lines)199 files (17,787 lines)
Source / test split2,623 / 4,0332,584 / 4,4715,207 / 8,504
Clojure1,558 lines1,000 lines2,558 lines
OpenTofu568 lines537 lines1,105 lines
Ansible YAML618 lines1,079 lines1,697 lines
Design rationale (plans/)291 lines223 lines514 lines
Golden fixture files5446100
Git commits8816
Deployment tracked files141428
Desired-state keys573895
Reused ONCEno — green onlyno — green only0 / 2
Launcher byte-identical to payload2 / 2
Secrets in tracked filesnonenoneclean
Self-reported failed checks7613

Source volume came out within 40 lines across the two arms, and both landed on exactly 8 commits. What differs is where the work went: PostgreSQL put its logic in Clojure, MySQL put its in Ansible. Same total, different layer.

4. Verified Failover & Recovery

Both packages were required to survive losing a machine for real. Neither result is a simulation or a dry run.

ObservationPostgreSQL armMySQL arm
Fault injectedleader droplet powered off via cloud APISIGKILL on the primary's mysqld
Promotion~9s (TTL 30s)23s
Endpoint serving new primaryno DNS write required43s (reserved IP moved at 36s)
Data lossnone — pre-failover marker row survivednone
Old node rejoinautomatic, missed WAL fetched from R2automatic on systemctl start
Archive continuity358 WAL segments unbroken from segment 1 across both timelinesbinlogs from all three members, restore check replayed_beats: 8

The PostgreSQL deployment also re-ran ./green create resolved straight from the pushed SHA on GitHub, with no working-tree override — converging all six stages and proving the pin and the launcher copy work on their own rather than only on the machine that wrote them.

5. Friction You Only Find by Running It

Each arm hit exactly one engine-specific wall, and both were invisible to every offline check.

  • MySQL caps replication-channel passwords at 32 characters. The supplied credential was 40 (ERROR 3056 / 3972, probed on a live server). The agent neither truncated the secret nor asked for a new one: the replication account carries SHA-256(password)[:32] in hex — one deterministic expression read by both consumers, 128 bits retained, documented in four places.
  • A powered-off node black-holes the SYN. PostgreSQL's endpoint resolves to three addresses, and libpq without connect_timeout waits out the OS TCP retry — about two minutes — before trying the next one. The fix became validated desired state rather than a README note: client-connect-timeout-seconds, exercised by acceptance nine times over, with bb golden failing if it disappears. Measured at connect_timeout=5: 6/10 probes at ~80ms, 4/10 at ~5.1s, zero failures.

The most expensive single mistake was not infrastructure at all. The PostgreSQL convergence gate demanded state == running from all three members, but a healthy standby reports streaming, and under synchronous mode its role is sync_standby. The cluster was correct on the first attempt; the gate spent 7.5 minutes failing to notice and skipped every task behind it. Five of that arm's seven failures were its own checks being wrong rather than the system under test.

6. What This Means for Platform & AI Engineering

Three things are worth carrying out of this run.

  • The reuse path lost to a blank page. Every previously existing package in this workspace builds on ONCE's provider registry and compute stages. Both agents independently declined it and wrote their own DigitalOcean and Cloudflare templates, arriving at 568 and 537 lines separately. Two isolated agents rejecting the same abstraction on the same task is a signal about that abstraction's fit for multi-node topologies — not about either database.
  • "Deployed" is a weaker claim than "survived." Both packages passed every offline check before either had a failover bug. The 32-character cap and the black-holed SYN appeared only under a real fault. A benchmark that stops at --dry-run would have scored both arms as finished and shipped both flaws.
  • Determinism is the deliverable. The output that matters is not the running cluster — it is a SHA-pinned Package Skill whose launcher is byte-identical to its payload, whose golden fixtures fail if a provider template drifts, and which converges the same way from a clean checkout. Both arms produced that.