Blog / Engineering

Static Sites Now Stay Up While They Republish

Republishing a static site used to empty it before uploading the new build. Now the old site serves until the new one is in place, and changes show in seconds.

Mythex Team · 2026-09-29 · 5 min read

Republishing a static site on Mythex used to delete the live files first and then upload the new build, so for the length of the upload some visitors got a 404 page or a page whose scripts were missing. A failed upload could leave the site empty or half new. Now the new build is written over the old one, pages last, and leftovers are removed a few minutes later. A second fix makes a republish show up within seconds instead of a minute — or, for files like a logo, up to a day.

How a static site is served

A static site is one that's just files: HTML pages, JavaScript, CSS, images. When you publish one on Mythex, we build it, upload the output to object storage under a folder for your site, and serve it through an edge network — servers close to your visitors that fetch files from storage and keep copies of them for a while. (If you're new to this, our guide on what a CDN is covers the idea.)

Two things were wrong: the order in which a republish changed the files, and how long old copies were kept.

Problem 1: the upload emptied the site first

A republish deleted everything in the site's folder, then uploaded the new build. For as long as the upload took — 12 seconds for a 250-file site in our local test — a visitor whose request wasn't already cached at the edge got a 404, or an HTML page whose scripts returned 404.

A failed upload was worse. The folder was already empty, and on one of the two upload paths the new index.html could land before the file that failed. A publish reported as failed could leave a half-new site live.

The fix: write over, pages last, prune later

  1. Don't delete first. The new build is uploaded on top of the old one.
  2. Assets before pages. A page names the scripts and styles it needs, so pages go up last. Until the new pages land, visitors get the old page, which names the old assets, which are still there. Once they land, the new assets they name are already in place.
  3. Remove leftovers later. Before the upload, we list what the site is made of. Three minutes after a successful upload, anything in that list that the new build didn't rewrite is deleted. Three minutes is longer than the 60 seconds a page could be cached at the edge and in the browser, so a visitor still holding the old page can find the assets it names.
  4. If in doubt, delete nothing. If the listing before the upload fails, nothing is pruned. Leftover files are harmless, and the next publish clears them. Deleting without knowing what was there is how a site ends up empty.

A failed upload now leaves the previous site exactly as it was.

We tested this end to end locally, using the real upload script, the real upload route and storage serving, and visitors arriving with a cold edge cache. With the old code, 55 of 91 visits were broken during a republish and 61 of 92 during a failed one. With the new code, 0 were broken across about 3,500 visits covering both upload paths, a failed upload, and the publish after it — and exactly the leftover files were pruned.

Problem 2: a republish took too long to show

Even when the upload went fine, changes were slow to appear. The edge kept its copy of a page for 60 seconds and of every other file for a day, and told browsers to do the same. Files with a fixed name — logo.png, a font, anything from a public/ folder — could show the old version for up to a day after a republish.

The fix: file copies under the build

When the edge asks our API where a site lives, the answer now includes a build marker that changes each time a publish lands. The edge files its copies under that marker. A publish therefore becomes a brand-new set of cache keys as soon as the edge's 30-second lookup expires. Old copies are simply never found again, so the edge can keep copies for a day without ever serving one past the publish that replaced it.

Browsers get two different rules:

  • Hashed build output — files like assets/index-CzLcxDO8.js, whose name changes whenever the content does — is cached for a year and marked immutable. It can never be stale under that name.
  • Everything else — pages and fixed-name files like a logo — is marked no-cache, which means "check before using". If nothing changed, the answer is a tiny 304 Not Modified with no body, now that we honour the browser's If-None-Match check.

Telling the two apart needs care: a file copied from public/assets/ keeps its own name, and caching assets/logo.png for a year would pin the old logo for a year. So a file only counts as hashed if it's in a build output folder and its name has a segment that looks like a hash, not an ordinary word.

Locally, the new page and logo were visible 6 seconds after the publish went live. Before, it was 60 seconds for the page and a day for the logo.

Two smaller fixes along the way

  • Blank pages from HEAD requests. A HEAD request asks for a page's headers without its body. We were caching those empty responses, so the next visitor to that page got a blank page. HEAD responses are no longer cached.
  • A real page for "nothing here". An address with no app used to get a plain-text "This published app is offline or the link was not found" with a 502 error code. It now gets a proper page, "There's no app here", with a 404 status and a way back to Mythex for the owner. Unknown custom domains get the same page.

What we took from it

  • Order is part of correctness. The same files uploaded in a different order are the difference between a broken site and a working one.
  • Delete late, and only what you know. Pruning with a listing taken before the upload is safe; emptying a folder and hoping the upload finishes is not.
  • Cache by version, not by time. Expiry times are a guess about when content changes. A key that changes with the build is a fact.

Related: your published app now stays up while you republish it. More on publishing in the docs.

Keep reading

  • A Failed Republish Never Deletes a Working Backend — A failed republish could delete an app's live API, or leave its broken new version running. Three fixes to how Mythex undoes a publish that goes wrong.
  • Alerts That Fire Once, Not Once per Server — A once-a-day alert reached us five times in one afternoon. Why in-memory dedupe breaks with two servers and a deploy, and how shared state fixed it.
  • Moving to Bigger Workspaces Without Making Anyone Wait — A quarter of our workspaces were stuck at 1 GB after we moved to 2 GB. Our first fix made one person wait seven minutes. What we changed, and changed again.
  • Billing Databases by What They Actually Use — We used to estimate each app's database cost, and the estimate was wrong both ways. Now every database is billed from measured usage, hour by hour.
  • Catching Apps That Crash Right After Publishing — Three publishes passed our health check while their API crashed seconds into every boot. The cause: counting crashes in a list capped at five entries.
  • Keeping Image-Heavy Agent Turns Within Memory — An agent turn that kept looking at catalogue scans ran out of memory at 952 MB. Three separate causes, each holding images too long, and how we fixed them.

Start building free · Templates · Docs