There is a line near the top of my monorepo's instructions file that I wrote myself, in bold, under a heading that says "Non-negotiable constraint." It says site creation must never wait on AI. The site builder creates a site immediately with defaults, and the AI results (themes, SEO, legal pages) arrive later and upgrade it. If the Mac Mini that runs my agent is unplugged, sites still get created.
I wrote that rule in July, tested it in production by turning the Mini off, and felt very good about myself.
Then in August I built a health feature and broke the rule in the first week, and it took three separate bug fixes before I noticed I had.
The feature
The health block is a private page on one of my sites. You upload a lab report PDF, an agent on the Mini parses out the markers into dated rows, and then you can ask questions about your own results in plain language. The parsing was always asynchronous: upload, get a job, poll for it. That part inherited the rule for free because it was obviously slow.
The chat did not. Chat felt like chat. You type, the model answers, you read it. So the send handler called a server action, the server action called the model, and the whole request sat open for the 30 to 50 seconds the turn took. On my desk, on wifi, one hop from the Mini, that is a slightly slow web page. It worked. I shipped it.
Bug one: thinking forever
The first report was that "Claudia is thinking..." never stopped. Not slow. Permanent, until you reloaded.
The handler awaited the server action with no try, no catch, no finally. When the platform killed the function mid-request and returned a 504, the promise rejected, the busy flag never cleared, and the spinner ran until the tab died. Worse: the conversation thread was only written to the database after a successful reply, so the failed turn did not exist anywhere. You typed a question, watched a spinner forever, reloaded, and found no record that you had ever asked anything.
I fixed it the way you fix things at 11pm. A try/catch/finally so the busy flag always clears and the error is retryable, plus maxDuration = 240 on the page routes that host the block so the platform stops killing the function early. Both of those are correct. Neither of them is the fix.
Bug two: the 8.7MB report
A few days later I tried to upload a real lab report, sixteen pages, 8.7MB, and the browser refused before it even started: "this file is large and may not upload."
The upload went through a server action, which means the file was riding inside the request body, which means it was capped at Vercel's roughly 4.5MB function body limit. My own client-side guard was warning me about a ceiling I had built into the transport by accident.
The fix here was structural and I already had the pattern, because audio and images in the same app do it correctly: ask the server for a one-time signed upload URL, have the browser PUT the bytes directly to storage, then call a small finalize action that creates the parse job. Raise the app ceiling to 25MB and raise the storage bucket's limit to match so the two agree instead of disagreeing quietly.
What changed is that the request stopped carrying the payload. It got permission, then got out of the way.
The shape
Writing that second fix is when I saw it. Both bugs had the same geometry.
In bug one, the request was carrying a duration: 30 to 50 seconds of model time, held open across a mobile connection that can end because a phone changed towers. In bug two, the request was carrying a volume: 8.7MB of PDF pushed through a pipe with a 4.5MB throat.
An HTTP request is the most fragile object in the entire system. It has a timeout I do not fully control, a body limit set by my host, and a network path that belongs to whoever is holding the phone. And I had put both the slow thing and the big thing inside it.
The rule I wrote in July was not really about AI. It was about that.
The rebuild
So the chat got rebuilt into the same enqueue and poll shape as everything else, and it is genuinely a better feature now, not just a more correct one.
Sending persists the user's turn and creates the thread immediately, before any model work happens at all. That single change fixes the worst part of bug one on its own: the question is durable the instant you hit send, and it shows up in the chat list right away, whether or not the answer ever arrives. A queued row goes into a jobs table. The worker on the Mini picks it up, runs the turn, writes the raw reply back. The phone polls every two seconds with a four-minute ceiling, and a job stuck past five minutes gets reaped instead of hanging.
The phone now makes a series of very short requests instead of one very long one. Nothing it does is fragile enough to matter.
There was an unplanned benefit. The chat accepts images, and synchronously those images just lived in memory inside a request, which felt simpler but had no policy attached to it at all. Going asynchronous forced them to have a real address: staged to a private bucket, downloaded by the worker, deleted after the turn completes. Health data never sits anywhere without a lifecycle. I would not have written that rule if the architecture had not forced me to name a place to put the file.
Why I broke it
The honest answer is that the synchronous version worked. It worked on my machine, on my network, at my distance from the Mini. The conditions the rule protects against are conditions I was not in.
That is the useful thing to take away, and it generalizes past this feature. An architectural rule is not there to stop you from writing something that fails. It stops you from writing something that succeeds under exactly the circumstances you happen to be testing in. I had a rule that would have caught this before the first line of code, in a file I read every week, and it did not fire, because the feature did not look like the thing the rule was about.
Now it reads differently to me. Not "never wait on AI." More like: if you find yourself putting either minutes or megabytes inside a single request, you have quietly made the whole feature a dependent of the most breakable thing you own.

