Shipping a Godot Game to the Web: Five Things That Broke

Published August 1, 2026 · Devlog · About a 10-minute read

Cell Dodge was originally a Godot 4 build compiled to WebAssembly, while the rest of this site is hand-written single HTML files. Getting it into a browser tab was harder than writing the other three games put together, and almost none of the difficulty was in the game code. These are the five failures that cost the most time — and, at the end, the reason I eventually threw the whole thing away.

None of this is a complaint about Godot, which exports to the web genuinely well. It is that the web is a hostile target with its own rules, and the failure messages rarely point at the actual cause. If you are about to do the same thing, this is the list I wish I had read first.

1. Opening index.html directly never works

The export produces an index.html, so the natural first move is to double-click it. It fails, usually with an error that looks like a broken build.

The build is fine. Opening a file from disk gives the page a file:// address, and browsers apply much stricter rules to those than to pages served over HTTP. The engine cannot fetch its own .wasm and .pck files under those rules, so it dies before the game starts.

A Godot web export must be served over HTTP, even locally. Any small static server will do. This costs people an astonishing amount of time, because the symptom (a scary console error) looks nothing like the cause (the URL starts with the wrong word).

2. Procedural audio goes silent — with a warning that explains nothing

This was the one I would never have guessed.

The music in Cell Dodge is generated at runtime rather than played from files, using an AudioStreamGenerator that the game writes samples into. It worked perfectly on desktop. In the browser it was silent, and the console said:

WARNING: ... is trying to play a sample from a stream that cannot be sampled.

The clue is the word sample. Godot can play audio two ways: as a "sample", handed to the browser's audio system to play as a self-contained sound, or as a "stream", mixed continuously by the engine. Short sound effects are cheaper as samples, so on the web that is the default. But a generator has no fixed contents to hand over — the whole point is that its samples do not exist until the game produces them. Asked to play as a sample, it plays nothing at all.

Two changes fixed it. Force the players themselves to stream:

player.playback_type = AudioServer.PLAYBACK_TYPE_STREAM

and set the web default in project.godot so nothing else silently falls back:

[audio]
general/default_playback_type=0
general/default_playback_type.web=1
The general lesson: the web export does not merely run your game in a browser, it changes some engine defaults to suit the platform. Anything generated at runtime — audio especially — deserves an explicit check on the web rather than an assumption that desktop behaviour carries over.

3. A single-threaded local server breaks loading

Having learned to serve over HTTP, I served over HTTP — with the simplest possible one-line Python server. The game loaded, but the browser's network panel showed the .pck request being aborted on every single load.

The cause is embarrassing in hindsight. The default simple server handles one request at a time. The engine asks for a large .wasm file and the .pck at the same time; the server is still busy streaming tens of megabytes of WebAssembly, the second request waits, and something eventually gives up.

The fix is to make the server threaded — in Python, mixing in ThreadingMixIn is enough:

class Server(socketserver.ThreadingMixIn, http.server.HTTPServer):
    daemon_threads = True
    allow_reuse_address = True

Worth flagging because it presents as a game bug — "the pck fails to load" — when it is entirely a property of the thing serving the files. Any real host handles concurrent requests; only the convenient local one-liner does not.

4. Enabling the PWA option kills a headless export

I wanted the game exportable from the command line, so it could be rebuilt without opening the editor. I also wanted the progressive web app option, so the page could be added to a phone's home screen. Those two wishes turned out to conflict.

With PWA enabled, a headless export failed with:

ERROR: Cannot export project with preset "Web" due to configuration errors:

— and then nothing. The list of configuration errors, which the editor would have shown, is not printed in headless mode. Running with --verbose added thousands of lines and not one of them about the actual problem.

Turning the PWA option off made the export succeed immediately. The requirement it enforces (icons of specific sizes) is reasonable; the part that cost the time is that a failing validation refuses to say which validation failed when there is no editor to display it in.

If a headless export fails on configuration, disable your optional features one at a time. It is a crude method, but faster than reading the log.

5. Threads, and the size of the download

Two decisions worth making deliberately rather than by default.

Thread support. Enabling it improves performance but requires the page to be served with specific cross-origin isolation headers. If those headers are missing or wrong, the game does not run at all. Since these games are meant to be opened by anyone on any phone with no explanation, I turned threads off and accepted single-threaded performance in exchange for the game reliably starting. For a bullet-hell running on modest hardware, that is the right trade.

Size. The engine build is around forty megabytes before any of my own content. That is the price of shipping a whole game engine to a browser, and it is why Cell Dodge takes noticeably longer to appear than the hand-written games on this site, which are single files measured in kilobytes.

It is worth being honest about that on the page itself rather than letting a visitor stare at a blank rectangle wondering whether it is broken. The other games open instantly; this one has to download an engine first, and saying so costs nothing.

The one that was not Godot's fault at all

An honourable mention, since it cost more wall-clock time than anything above. The export templates — the prebuilt engine binaries needed to export at all — are a download well over a gigabyte, and from where I am the connection to the host was throttled to a crawl. A single download would have taken about an hour.

The fix was not clever, just mechanical: fetch the file in a dozen byte ranges in parallel and stitch them together afterwards. It finished in a couple of minutes. Sometimes the blocker is not the engine, the framework or the browser — it is simply distance and a rate limit, and it is worth recognising that before spending an hour debugging something that is not broken.

What I would tell myself at the start

The game itself needed almost no changes to run on the web. Everything that went wrong lived in the space between the engine and the browser — which is, I suspect, the general experience of shipping anything to this platform.

Epilogue: I deleted all of it

Every problem above got solved. The game ran, the music played, the loading worked. And it still was not good enough, for a reason none of the fixes could touch: the forty-megabyte download itself.

On a desktop connection that is a couple of seconds and nobody minds. On a phone it is long enough that a visitor who arrived from a link has time to decide the page is broken and leave. Every other game on this site opens more or less instantly, so Cell Dodge was not just slow — it was conspicuously the odd one out, and the wait came before anyone could see whether the game was any good.

So I rewrote it from scratch as a single HTML file: plain canvas, plain JavaScript, procedurally generated audio, no engine at all. The result is around fifty kilobytes — roughly eight hundred times smaller — and it starts in well under a second on a phone. It has the same three bosses with three phases each, the same permanent upgrades, the same daily challenge.

Rewriting it took less time than debugging the export had. That is not a knock on Godot, which is a genuinely good engine and the right tool for a game with real content — art, animation, physics, scenes. It is that Cell Dodge never needed any of that. It is circles moving in patterns on a dark background. I had been paying the full price of an engine for a game that used almost none of it.

The lesson I actually took away: the question is not "can this engine export to the web?" — nearly all of them can. It is "how much of this engine does my game actually use?" If the answer is "very little", the download is a cost paid by every single visitor for features only the developer benefits from. Measure that cost before building, not after shipping.

The rewritten version is what you play at Cell Dodge today. Everything above is preserved because the Godot problems are real and the fixes are correct — if you are shipping an engine build to the web, they will save you an afternoon each. Just check the download size first.

The game this is about: Play Cell Dodge →
Free, runs in your browser, no sign-up.

Written from notes taken while building this site. Questions and corrections are welcome via the contact page.