I have watched a few accounts end. In almost none of them was the code the reason.
The build was fine. Late sometimes, imperfect sometimes, but fine. What went wrong was that the client stopped being able to predict what was happening, and prediction is most of what they are paying for.
The shape it takes
It rarely starts with a complaint. It starts with the client asking for an update slightly more often than they used to.
That is the signal, and it is easy to misread as a demanding client. It is not. It is someone whose confidence has dropped below the level where they can wait for the scheduled update, so they are topping it up manually. The extra ask is them compensating for something your process stopped providing.
Left alone it goes one way. Updates get requested more often. They get answered with more reassurance and less specifics, because the specifics are uncomfortable. The client starts copying more people in. Then there is a call about “how we’re working together” rather than about the work.
Late is survivable. Late and unexplained is not
Clients absorb bad news far better than most teams expect. A date moving two weeks, flagged early with a reason and a revised plan, is a normal conversation. It is a project, things move.
The same two weeks discovered by the client at the point it becomes undeniable is a different event entirely. Not because the delay is worse, but because they have just learned that their view of the project was wrong — and once that is established, every future update is discounted. They stop asking “is this on track” and start asking “how would I know”.
That is much harder to come back from than a slipped date.
Why good teams still get this wrong
Not through negligence. Because bad news has a natural delay built into it.
Nobody wants to escalate something that might resolve itself by Thursday. So the reasonable instinct is to wait until it is certain — and by the time it is certain, it is also late, and now the conversation is about both the problem and why it was not mentioned sooner.
The fix is not to try harder. It is to remove the judgement call. If a defined condition means a client hears about it — a task past its date by some threshold, a dependency unanswered for some number of days — then nobody has to decide whether this one is worth raising. The threshold decided already.
Teams resist this because it feels like it will generate noise. In practice it generates fewer, earlier, smaller conversations instead of one large one, and it takes the emotional weight out of escalating — which is the actual thing that was blocking it.