Back to Explore
NewsVibeFix 编辑部Updated Oct 12, 2026

DHH's AI-Rewritten Campfire Under Load: 1% Delivery — and Why You Still Have to Think

On October 11, Piotr Sarnacki published 'I'm sorry, but you still have to think' on It's all about the bit — a teardown of DHH's AI-rewritten Campfire Once, shipped unread. Rust delivered ~1% of notifications under load and hid blocking DB calls in async tasks; Elixir delivered better but hit 1.8GB of memory. One line — stream_capacity 256 to 16384 — delivery 14% to 90%, but pMAX latency 11s to 130s+. Sequel to our cycle37 DHH manifesto: AI made writing cheap; thinking is the scarce resource.

Illustration of DHH's AI rewrite experiment: the Rails original Campfire Once rewritten by AI into Rust, Elixir, and Go versions, with nobody reading the code

Lead: The Manifesto's Sequel, Not a Rerun

If you remember our cycle37 piece, "We No Longer Write Code by Hand": Rails Creator DHH's Manifesto for the Agent Era — in which DHH announced he had stopped writing code by hand, handing all of it to an AI agent he never bothered reading — then today's story is the aftersales chapter of that manifesto. Same protagonist, same experiment, new development: the community actually put DHH's AI-rewritten code under a real load test. The results are not flattering.

It happened on October 11. Piotr Sarnacki published a long teardown on his personal blog, It's all about the bit, titled "I'm sorry, but you still have to think" — a full re-verification of DHH's famous "AI rewrites Campfire Once" experiment. That day it was tagged vibecoding on Lobsters, and on October 12 it hit the Hacker News front page. The hottest debate in the comments wasn't "Rust vs. Elixir" — Sarnacki explicitly warns against falling into that trap. It was this: when AI writes all of your code, what is left for the engineer to be worth anything?

A quick alignment on the background, without replaying the manifesto piece: DHH had AI (he calls it "clanker") rewrite Campfire Once from Ruby on Rails into Rust — finding Rust "unbearable" to read or write, he never read the output at all — and later added Elixir and Go rewrites. Sarnacki's angle is sharp: this is a perfect natural experiment. An experienced veteran, writing code through AI agents while never reading it — what does the output actually look like?

The answer: not great. And this teardown's value isn't the cheap schadenfreude of "look, AI faceplanted" — it's that it organized the crash site into a failure-mode checklist for vibe coders. Every item on the list is something AI won't think about for you, but you must.

DHH's AI rewrite experiment: the Rails original Campfire Once rewritten by AI into Rust, Elixir, and Go versions, with nobody reading the code in between

Failure Mode 1: With a Vague Prompt, Every Architecture Decision Is a Coin Flip

Sarnacki opens with a truth most people never noticed: if your prompt isn't specific enough, a huge number of decisions are effectively decided by coin flips. Because many of these questions have no standard answer: should backward compatibility be preserved? Do you care more about latency or throughput? How much memory may the system use under load? After a crash, how many new-message notifications are you willing to lose?

You might not care about these questions, or only some of them. But when the LLM implements what it thinks you want, those questions get answered — quietly, by default. The three rewrites exposed this fast: the Rust version dropped CSRF tokens for caching convenience; for simplicity, it swapped Redis for an in-process queue (which is exactly where notifications flowed). The Elixir version stayed much closer to the Rails original. What does that mean? It means the "Rust vs. Elixir" comparison was void from the start — the differences had nothing to do with the languages; they were pure coin flips by an AI in places nobody had specified.

That's the first slap for vibe coders: you think you're comparing "which language the AI writes better," but you're actually comparing "how many coin flips the AI happened to call right this time." Constraints not stated in the prompt don't magically become correct architecture decisions — they become random ones. Next time you ask an agent to "rewrite this service," ask yourself first: backward compatibility, latency goals, memory budget, failure semantics — did you write those four into the prompt? If not, whether the output is good is pure luck.

Failure Mode 2: The Task That Never Yields, and the Blocking async

Next come the hard defects at the code level, found by Sarnacki reading the code — which is, ironically, exactly what an article titled "you still have to think" would do: find problems by reading code.

One criticism of the Elixir version in the community: it processes all SQL queries serially in a single process, including read queries that could have run concurrently. DHH's reaction was that it made Elixir look bad — AI can't write high-performance code, so blame the language? Sarnacki disagrees, because he looked at the Rust version: the problems there may be worse.

Some database operations in the Rust version aren't asynchronous at all — they're blocking operations running inside async tasks. In Rust's async runtime, scheduling is cooperative, not preemptive: a task that never yields keeps every other task on the same worker thread from running. Picture a 100ms database query sitting there waiting, and every queued task waits along with it. The right move is I/O that yields. Same with locks: hold a synchronous lock for 10ms and every task on the thread serves time with you, while the task holding the lock isn't even doing anything. The safe play is an async lock like tokio::sync::Mutex.

These aren't "the AI typo'd a variable" problems; they are concurrency-model comprehension problems. AI can memorize the tokio API, but it won't stop to think "will this query block the scheduler?" Sarnacki's verdict is measured: no, programming probably hasn't been "solved" yet — you still have to know what you're doing.

For vibe coders, there's an uglier truth hiding here: bugs like these are completely invisible on the happy path. All feature tests green, demo blazing fast — and then traffic rises, worker threads get nailed down one by one by those 100ms blocking queries, and the system collapses. When the AI wrote that code, it carried zero cognitive burden — it didn't know it was using cooperative scheduling; it was just snapping API calls together. If you don't read the code, you'll never know there's a time bomb buried here. And when production blows up, you won't even know where to start, because "the code was written by AI" is approximately equal to "nobody actually understands this code." A system nobody understands produces mystical bugs.

Cooperative scheduling in Rust's async runtime: one task blocking 100ms on a database call pins the entire worker thread; everything else waits

Failure Mode 3: Backpressure and Failure Strategy — The AI Thought of Neither

This is the best section of the piece: a story about how one line of code changed everything.

The Rust version broadcasts new-message events to all online clients over a tokio::sync::broadcast channel. That channel is bounded: once full, lagging receivers get a Lagged error and messages are silently dropped. In the rewrite, that capacity was set to 256. That's why Zach Daniels measured only ~1% delivery of new-message notifications in the Rust version under heavy load — not a networking issue, not a logic bug; the backpressure policy, in the absence of anyone specifying it, defaulted to "quietly drop messages."

Then Sarnacki did something elegant: he changed one line, stream_capacity from 256 to 16384. Under a load test of 100 requests per second, delivery jumped from ~14% to ~90%. If this were a marketing blog, the story would end with "one line, 6× performance." But a real engineer reads the second metric: pMAX event delivery latency degraded from 11 seconds to over 130. Messages stopped being dropped — but users got their "someone sent a new message" notification two minutes late.

Which is better? Sarnacki's judgment is clear: fail fast — let lagging clients disconnect and reconnect. After reconnecting, the browser just fetches the latest messages; it never needed those late events at all. Compare the Elixir version: its GenServer mailbox is unbounded by default — no messages lost, but memory climbed to 1.8GB during the stress test. So the ultimate question: would you rather lose some messages, or be killed by the OOM killer?

Note what makes this reasoning chain valuable: it isn't "tuning a number," it's first answering "what is the user experience supposed to be?" For a chat app, "reconnect and see the latest messages" is a perfectly acceptable, even invisible experience; "notifications arriving two minutes late" is a direct hit on the product's core value. When the AI wrote the broadcast logic, it couldn't have made that product judgment — it didn't even know this was a notification channel for a chat app rather than a background queue that can be consumed slowly. The same capacity of 256 might be perfectly fine in a log-collection scenario and a disaster in real-time notifications. Arguing about the "correct configuration" divorced from business semantics is itself a pseudo-question.

How big is the right backpressure, what to do with clients that can't keep up — drop or queue — which is worse, slowness or loss? The AI thought about none of these three questions when writing the code, because none of them were in the prompt. This is the lesson vibe coders should steal most: any decision about "what happens when the system is overloaded" is almost always wrong by default, and AI will only ever give you the default.

Failure Mode 4: Benchmarks Are Hard — Single Metrics Are Liars

Sarnacki spends a lot of space on benchmarking methodology, because the loudest numbers in this saga are also the most deceitful.

Start with the original "1% vs. 100%": Rust delivered 1% of notifications, Elixir 100%. Plenty in the Elixir community popped champagne: Elixir is the concurrency king! But wait — the Rust version processed 6,000–7,000 events while Elixir processed about 1,700. The denominators aren't even comparable. The only thing the comparison proved was that Elixir handled backpressure better — on the condition that the load hadn't crushed it yet.

The deeper problem is the testing method itself. DHH and Zach both used closed-loop testing: N clients, each firing the next request the instant it gets a response — measuring "how much can the system swallow per unit time." But real-world traffic spikes look like many users all acting at once, nobody waiting for anyone — which is when you should test with constant arrival rate. Sarnacki re-ran the test at a constant 100 POST/s, and the story flipped: Rust delivered ~14%, Elixir ~60%. Elixir still wins — hold on, not so fast: ~23% of Elixir's HTTP POST requests simply timed out, and its worst event delivery latency approached 180 seconds.

So the question changed from "whose delivery rate is higher" to "which user experience do you want": a client silently reconnecting in the background and pulling the latest messages for free, or "new message notification arriving three minutes late" — pick one. A single metric gave us "1% vs. 100%," a simple, wrong, inflammatory answer; multiple metrics assemble a map of trade-offs. Sarnacki poured cold water on those rushing to declare "Elixir crushes Rust": declaring language superiority off one metric, without examining root causes, misleads people.

This section carries a double warning for vibe coders: first, don't trust a benchmark screenshot an AI ran for you — "2× throughput" might just mean it changed the failure policy to "quietly drop"; second, when you ask an AI to "optimize performance," if you don't specify "optimize under what constraints, while protecting which metrics," it will trade away the metric you care about most for the one you never mentioned.

There's a third layer, more insidious: Sarnacki points out that in the original closed-loop test, the Rust version handled 4× Elixir's request volume — and merely "handling more requests" already made its notification-delivery workload heavier. In other words, even the question "whose architecture is better" was quietly rigged by the test harness. Methodological asymmetry systematically manufactures wrong conclusions, and wrong conclusions with pretty numbers spread ten times faster than the truth. That wave of "we won" cheering in the Elixir community came from exactly that. Which is why Sarnacki re-ran the constant-arrival-rate test himself: he doesn't trust other people's benchmarks — he only trusts numbers produced by a methodology he understands.

Three Things for Vibe Coders: The Faster AI Writes, the More You Must Know These Three

Compress the teardown into an action list and you get three items. Coincidentally, all three are things the "AI writes code" narrative assumes you no longer need.

First, write the trade-offs into the prompt. Preserve backward compatibility? Latency or throughput first? Memory ceiling? Is losing messages on crash acceptable? These questions have no standard answers — and what you don't write, the AI decides by coin flip. Sarnacki's line belongs on a wall: when the prompt isn't specific, most decisions are coin flips. Before your next greenfield project, spend half a page on non-functional requirements — ten times more effective than any prompt trick.

How to write it concretely? No essay needed — four fill-in-the-blanks suffice: "This service must stay compatible with ___ (old clients / old APIs / nothing); latency target is ___ (p99 in ms), throughput target is ___; memory budget is ___, and beyond that we ___ (reject / degrade / alert); on failure, prioritize ___ (never lose data / stay available / stable latency)." Fill those four blanks and the AI's coin-flipping room shrinks to almost nothing. Most complaints that "the architecture AI writes is always slightly off" trace back to these four blanks left empty.

Second, think through the failure modes before letting AI touch the keyboard. When the system is overloaded: drop messages, queue them, or reject outright? For clients that can't keep up: let them wait slowly, or disconnect them so they reconnect? That 256-capacity broadcast channel in the Rust version is a monument to "nobody thought about failure modes." AI gives you code that runs on the happy path; the failure mode is the part you must specify by hand as an engineer — that part cannot be outsourced.

Third, learn to run benchmarks, not just read them. Closed-loop or constant arrival rate? Are the denominators aligned? Besides throughput, did you look at latency? At p99/pMAX? At memory? At error rates? "Benchmarks are hard" is not a lament — it's a methodology warning: a single metric will always lie; the only question is what it wants you to believe this time.

The funny thing is, these three get less glamorous as they go: writing constraint docs, drawing failure flowcharts, building load-test harnesses. None of them will make a demo sparkle, but missing any one of them means your "AI-written system" is running naked. Which is probably why Sarnacki chose that title — it's not "AI sucks"; it's "the thinking part can't be delegated to AI."

Our Take: The Manifesto and the Teardown Are Two Sides of the Same Coin

Read this alongside our cycle37 DHH manifesto piece and a beautiful symmetry appears. In the manifesto, DHH says "I no longer write code by hand" — a declaration about writing; in the teardown, Sarnacki says "but you still have to think" — a reminder about thinking. One is about production, the other about acceptance. Together they form a complete theory of AI programming.

The most telling detail: DHH is no novice. He's the creator of Rails with decades of experience, and even his output — shipped without being read — carried defects like blocking async runtimes and default-drop backpressure. That tells us the problem isn't "the user isn't skilled enough"; it's the act of "not reading the code" itself. Experience helps you write better prompts, but it can't replace critical review of the output. AI turned "writing" into a cheap commodity, so "thinking" became the scarce one — and scarce things only get more expensive.

So this isn't an "anti-AI" article, and Sarnacki isn't picking sides — he says he likes Elixir and has used it in production. What he's really saying is that closing line: think critically, don't be fooled by single metrics, know when a system should fail, and benchmarks are hard. Translated for vibe coders: you can let AI write the first draft, but the question "is this production-ready" can only be answered by you.

The manifesto said "AI writes it, fine." The teardown said "after writing, you still have to think." Read both, and you hold the complete puzzle of this saga.

Views 0Comments 0

Comments (0)

ME
0/1000
Loading comments...

Sources

Browse projectsPublish your project

Related articles

Illustration of an indie developer swapping a chip in a robot on a workbench, symbolizing the model-swap process for AI projects
Guide
Model Retirement Survival Guide: A "Model Swap" SOP for Solo Devs

October brought two Copilot retirement waves, GPT-5.5 leaving Codex, and 17 OpenAI API model identifiers shutting off — model swaps are now operations, not upgrades. This field guide delivers a complete swap SOP: three abstraction layers, a 30-minute minimum eval baseline, a four-step shadow-to-full rollout, 10-minute weekly notice monitoring, the true cost formula, three real failure modes, and a one-page copy-paste checklist.

Developer WorkflowModel UpdatesIndie Development
The new TRAE's dual-mode interface: Agent mode runs multiple task cards in parallel while IDE mode keeps full code editing, switchable in the top-right corner
News
TRAE Merges Work and Code: ByteDance Bets on a Unified Agent Workflow

On October 9, ByteDance merged TraeWork and TraeCode into a unified TRAE: one entry point for continuous tasks, one-click switching between Agent mode and IDE mode. Agent mode is a global workbench where multiple agents split coding, testing and fixing; IDE mode keeps full editing for detail work. All deliverables settle into a Project Artifacts hub instead of chat history. The real story: 'work' and 'code' in one agent workflow, plus a labor-division playbook for one-person teams.

Developer WorkflowTool TipsAI Coding
Stack Overflow's 'the agent is the model plus its harness': LangChain's coding agent gained 13.7 points on Terminal-Bench 2.0 with the same model, harness-only changes
News
The Model Is Only a Tenth: Stack Overflow on "Agent = Model + Harness"

Stack Overflow's engineering blog (Oct 7) argues the agent is the model plus its harness - and the model is maybe a tenth. Evidence: LangChain's agent jumped 52.8% to 66.5% on Terminal-Bench 2.0, from outside the top 30 to the top 5, changing only the harness. The essay prescribes six artifacts - constitution, autonomy boundary, tiered CONTEXT.md, durable memory, a multi-lens review panel, a PostToolUse hook - plus multi-agent coordination via shared state and an append-only decision log.

Industry TrendsDeveloper WorkflowTool Tips