At 9:01pm one night a single web server binding disappeared. Every customer custom domain on our platform returned a bare 404 until 8:11 the next morning. That is about eleven hours.

We had monitoring. It checked customer domains on a five minute loop, and a failure on it was wired to an external uptime service that phones people. It never fired once.

This is not a story about one bad command. The command is the least interesting part. The interesting part is how many separate things all had to be true for a single removal to sit there overnight, and how each one was a layer we had assumed was covered.

What broke

All of our customer custom domains land on one IIS site through a catch-all HTTPS binding. One binding, on one IP and port, with no host name, so that any customer domain the CDN sends us is accepted. That is a sensible way to run a multi-tenant platform. It also means one object carries every customer.

An AI coding agent made the change. We had dispatched it to set up a new internal site on the same server. It took a configuration backup first, which is the only reason the fix later took one command. Then it created the new site. Its first script errored on a binding that already existed. Its second script removed the new site’s HTTPS binding so it could recreate it with a different setting, and the removal named the new site and the protocol, and nothing else. No address, no port, no host.

The server config history shows the catch-all binding gone in that same minute, and the new site’s own binding intact. When the agent re-ran that script, the removal failed with “cannot find binding”, which fits the first run having removed something other than what the agent meant.

We have to be straight about the evidence here. The exact cmdlet is inferred from timing and from the transcript. We deliberately did not reproduce its matching behavior on production to prove it. What we can show is the before and after of the config, not the mechanism.

The agent then reported the new site verified. It had loaded the new site and nothing else.

Here is one customer request, hop by hop, and where the monitor looked versus where it broke.

Diagram of one customer request: the customer types a domain, the apex relay answers with a redirect to www, the CDN carries the www request, and the web server's one shared catch-all binding serves every customer. The monitor looked at the apex relay, which stayed healthy. The www hop was never requested. The shared binding was removed at 9:01pm and every customer got a 404.

Layer 1: detection coverage

This is where we were most sure we were covered, so it is where we were most wrong.

The five minute monitor had four test domains, put there on purpose as stand-ins for customers. When we asked afterwards why they did not trigger, the answer was plain. Each probe requested the apex form of the domain, with redirects switched off, and expected a 301 to the www form.

The apex is answered by a separate small relay server. The relay was healthy all night, so the monitor correctly reported what it checks. But customers do not live on the apex. They live on www, which goes through the CDN into the web server that broke. The monitor stopped at the redirect and never made the second hop.

The check was even labelled as the relay check, with the four domains listed under it as sub-results. At a glance it looked like a full domain check. It was not.

Where it goes wrong: a probe proves only the exact request it makes. A redirect proves the redirector answered. A 200 proves something answered, not which something.

A second detection signal we were not using: the support inbox. While the monitor stayed green, our customers noticed. Dozens of emails from concerned users arrived overnight and sat in the inbox. We only saw them after a staff member reached out in the morning. The inbox was an alarm nobody was listening to. Your customers noticed hours before your tools did, and their emails were sitting in an inbox.

A sudden jump in support tickets, emails or chats is a detection signal in its own right. It does not care which host form is probed or what a health page reads, so it is a backstop for the day the synthetic monitor is blind. It should raise an alert, including outside business hours, to someone who will act on it. We have not built this yet.

What goes here: probe every host form a customer actually uses, through the same path a customer takes, and require content that only the right backend can produce. We added a www check for each test domain, with redirects not followed. It requires a 200 from a tiny marker page that returns which IIS site answered and a one-time token we send, plus the CDN’s own request header, so a cached copy, a default page or a wrong site all fail. The next addition is the inbox alert above.

Layer 2: the health signal

We assumed that once the monitor was fixed we were done. Then a code review turned up something worse.

An external uptime service polls one health page every five minutes and phones people when it stops saying “healthy”. That page reported healthy whenever the latest finished monitor run existed. A run that finished with a failure is still a finished run. So the page could not report a failure from any monitor, ever. We checked this against production data and found failed runs from the week before that still read healthy.

So the chain was broken in two places. The monitor never looked at customer www, and even when it did fail, the page the phone service reads would have said everything was fine.

Where it goes wrong: a health page proves only what it reads. Stale results are the quiet version of the same bug: if the monitor itself stops running, the last good result sits there looking healthy.

What goes here: the health page now requires the latest run to have passed, reports an error if the newest finished run is more than twenty minutes old (our real gaps never exceeded eleven), and treats a run that was closed out after timing out as a failure rather than the pass the engine had been storing.

Layer 3: reaching a human overnight

The uptime service stayed green all night, so nothing escalated. Nobody was paged, because the page it reads said healthy. The first person to reach us was a staff member, in the morning. Only then did we see the dozens of emails from concerned users that had come in overnight.

When we fixed the layers above, we wanted proof the last link worked, not an assumption. So we ran a deliberate, announced test: point the new check at a domain that does not exist, let the monitor fail, and watch the whole chain. The check failed, the health page flipped, the uptime service marked us down on its next poll, the emails went out on the second failed check and a phone rang.

It also found a bug no unit test would have. We wanted a WhatsApp message on the second failure, because the service cannot send one. We built a small watcher on one of our own machines for it. Its test outage message arrived without the “TEST RUN” label we had put on it, and claiming the phone tree had been called, which the watcher cannot know. The cause was a model that reviews every outbound alert and had rewritten the text. Health check alerts now go out word for word.

Where it goes wrong: an alert proves nothing until a human is shown to receive it. Software that edits alert text can lose the part that matters. And any step that lives on one machine fails when that machine does. The watcher runs on one machine, so it is a convenience, not a guarantee. The phone calls do not depend on it.

Layer 4: blast radius

One binding, every customer. That is the structural weakness that made this an all-customer outage instead of a one-site problem. We are not going to change that architecture this week, because the shared binding is what lets us run on one certificate and no per-customer config.

What we can do is make the object loud. It is now listed with a do-not-touch mark in a single page that enumerates every site and binding on the box. While reading the server we also found a smaller related follow-up on the same box. Nothing is broken today, and it is queued.

Where it goes wrong: any object that serves more than one tenant is a single point of failure, and you usually find the list by getting burned by the first entry.

Layer 5: preventing the change

No document said never to remove that binding. It was described in several places, but only described, and none of them was the runbook for changing the web server. The agent had read a registry note describing the catch-all binding sharing that IP with another site, and then made a change that removed it.

A written rule that an agent has to read and choose to obey is a weak layer. The sanctioned path now is a wrapper script: it takes a backup, snapshots every site and binding, runs the change, diffs everything against the snapshot, loads the customer domains, and restores automatically if anything outside the target changed or two of four domains fail. A binding can only be removed by giving the full address, port and host, and the shorthand form is refused outright. We ran it with a dry run and its self-tests on the real server before trusting it, and an outside model reviewed it. The review found real bugs, including paths where it could have left a bad change live. Those are fixed.

A guard is now live in our own tooling: a command from our agents that changes the production web server is refused unless it goes through the wrapper, and a command it cannot read is refused too. It covers the routes our own agents use, so treat it as a strong default rather than a promise that no other route exists.

Layer 6 and 7: verifying and rolling back

The agent verified the thing it changed and nothing else. The fix for that is in the wrapper: verify a sample of unrelated customer domains against the before state, every time. The backup the agent took before starting is what made recovery a single step. The binding was restored exactly as it had been, with no restart or reboot, within minutes of the report.

Here are the six stages in the diagram side by side, with what failed in each and what we put in place.

Table of six layers: prevention, detection, health signal, alerting, response and recovery. For each, what went wrong in the incident and what we put in place. Prevention: a removal named only the site and protocol; now a wrapper is the sanctioned path and a guard in our own tooling refuses changes outside it. Detection: the monitor probed the apex relay and dozens of customer emails sat unread overnight; now a www check needs a marker from the right site, and an alert on a spike in support email is not built yet. Health signal: healthy meant a run had finished; now it means the latest run passed and is recent. Alerting: nobody was paged; now an end to end test proved the phone rings. Response: a staff member reached out in the morning; now one page lists every site and binding. Recovery: a backup made the fix one step, but there was no automatic rollback; now the wrapper restores by itself.

What we would tell you to copy

Do not copy the fix list. Copy the order of questions.

  1. Does your monitor request the exact host forms your customers use, apex and www, every tenant pattern, and require something only the right backend returns?
  2. Do you watch the rate of inbound support tickets, emails and chats, with an alert on an abnormal spike outside business hours, and who receives it?
  3. Can your health page ever say healthy while the last run failed or is stale?
  4. Has anyone proved, with a real test failure, that a human is reached at night?
  5. What is the one object that serves every tenant, and can an unrelated change reach it?
  6. Is there a path that makes the safe change the only change, and something outside the agent that enforces it?
  7. After a change, do you check the things you did not touch?
  8. Can you restore in one step?

The prompt below runs those questions against your own system, layer by layer, starting with the host forms. It is read only.

Eleven hours, one shared binding, and a monitor that only ever asked the apex relay. Every layer above had a reason to be trusted, and the first alarm that actually rang was a staff member in the morning, after dozens of customer emails had sat unread in the inbox all night.