There is a moment every tool-site owner dreads: your provider's API goes down, and a visitor is staring at your page wondering whether anything is happening. For a long time, my answer to that moment was a spinning loader. It felt polite. It was not. A spinner says "something is happening" when nothing is, and after thirty seconds the visitor leaves — not because the tool failed, but because I would not tell them it had.

I run ramrom.io, a small video downloader built in plain PHP on shared hosting. Behind the scenes it leans on third-party provider APIs to resolve TikTok, Instagram, Facebook and YouTube links. Those providers go down more often than anyone admits. Some days they are slow, some days they return a blank response, and some days they cheerfully reply "Service is busy" to every single request. I cannot fix any of that from my side. What I can control is what the visitor sees while it is happening.

So I changed the rule: the page never waits in silence. My resolve endpoint, resolve.php, gives a provider call a short timeout — a few seconds, not thirty. If the provider does not answer in time, or answers with garbage, the endpoint returns a plain JSON error with a reason in plain words: the provider is down, the provider is busy, or the link could not be understood. The front end then does the honest thing: it stops the loader, hides the empty quality dropdown, and shows a short message explaining what happened and what to try next — check the link, wait a minute and try again.

That sounds trivial, and it is, which is exactly why it took me too long to do it. The hard part was not the code. The hard part was accepting that an honest error is a feature, not a failure to hide. A spinner is a promise. If the promise is a lie, the visitor does not think "the provider is down", they think "this site is broken". A clear message — "the service we use to fetch this video is busy right now, please try again in a minute" — tells them the truth, keeps them from hammering the button, and costs me nothing.

There are three states a visitor can actually be in, and I now design for each one separately. First, the link itself is bad — deleted video, private post, pasted link from the wrong platform. That gets a specific message: the video could not be found, or the link does not look like a supported platform. Second, the provider is up but struggling — slow, or returning "Service is busy". That gets a retry message and a short wait, because the same link often works a minute later. Third, the provider is fully down. That gets a plain statement, no fake progress, no countdown timer pretending I know when it will be back.

I also stopped the front end from making the problem worse. The resolve call is debounced, so a fast typist does not fire a dozen requests. The download button stays disabled until there is a real file to fetch — there is no point letting someone click a button that cannot do anything yet. And when an error is shown, the error is the end of that attempt: no automatic retry loop in the background burning through my API quota while the visitor has already gone.

One detail I am glad I kept: the errors never blame the visitor. "Something went wrong" is the laziest sentence on the web, and "invalid URL" is not much better when the URL was perfectly valid and the provider was the thing that failed. If the problem is mine — or my provider's — the message says so. People are far more forgiving of a tool that admits a fault than of one that pretends everything is fine while doing nothing.

The lesson is small and it generalises: graceful degradation is not a design pattern, it is a promise to tell the truth about the state of things. A loader is fine when work is actually in progress. The moment work has stopped, the loader has to stop too, and something honest has to take its place. On a side project with no budget and no status page, that honest message is the whole of your reliability story — and visitors notice the difference, even if they never say so.