On November 14th 2025, Cloudflare had a massive global outage. Half the internet went down. The memes were instant and brutal. I thought it would be funny to make a game about it.
The Premise
Cloudflared is a tower defense game where you’re defending Cloudflare’s lava lamps from incoming web threats. If you didn’t know, Cloudflare literally uses a wall of lava lamps as a source of randomness for their encryption. If the lava lamps go down, the internet goes down. That’s the plot.
Instead of gold and lives you manage Budget (currency) and RAM (tower capacity). Your towers are all named after real security concepts: Rate Limiters, WAFs, Deep Packet Inspectors, Honeypots, IAM, Reverse Proxies, and Patch Servers. The enemies are cyber threats: Script Kiddies, Worms, DDoS swarms, Zero Days, Botnets, Trojans, and the dreaded .unwrap() final boss (if you know, you know).
The upgrade names are equally nerdy. You can upgrade your Antivirus into “FalconOne” (yes, that CrowdStrike reference) or your IAM into “Keycloak” or “Kerberos”. The system upgrades include “Download More RAM” and “CISO Training” for passive income.
Mobile First
I wanted this to be something you could pull up on your phone and play while bored. The game canvas runs on Konva (a 2D canvas library for React) and the UI is all Tailwind. On mobile, the audio system runs a smaller pool size and staggers preloading to keep the main thread from choking:
const POOL_SIZE = isMobile ? 2 : 3;
// stagger audio preload to avoid blocking
keys.forEach((key, idx) => {
window.setTimeout(() => {
const audio = pool?.[0];
audio.muted = true;
audio.play().then(() => {
audio.pause();
audio.muted = previousMuted;
});
}, idx * 80);
});
The HUD rearranges itself on small screens with a compact overlay in the corner showing wave number, threats remaining, and lava lamp health. Touch targets are sized for thumbs, not mouse cursors.
The Service Worker
The whole game works offline as a PWA. Once you’ve loaded it once, you can play it on a plane. The service worker uses a dual caching strategy: network-first for the HTML shell (so you always get the latest version if online), and cache-first for all static assets like JS, CSS, images, and sound effects:
self.addEventListener('fetch', (event) => {
// ...
if (isNavigationRequest(event.request)) {
event.respondWith(
networkFirst(event.request).catch(async () => {
// Offline? Fall back to cached shell
const cache = await caches.open(STATIC_CACHE);
return cache.match('/index.html');
}),
);
return;
}
if (isStaticAsset) {
event.respondWith(cacheFirst(event.request));
}
});
This means the game assets (including all 25+ sound effects) get cached on first load and served instantly from cache on repeat visits. The app shell is always fresh when connected but still works completely offline.
Balancing Infinite Waves
This was the hardest part and took more iteration than I expected. The first 7 waves are handcrafted tutorials that introduce each enemy type. After that it switches to a procedural infinite mode with a 12-wave repeating cycle.
The scaling math is pretty aggressive. Enemy count multiplies by 1 + (infiniteLevel * 0.8) per wave and spawn intervals shrink by up to 95%:
const countMultiplier = 1 + (infiniteLevel * 0.8);
const speedFactor = Math.min(0.95, infiniteLevel * 0.15);
Boss enemies layer on top at set intervals. MOAA (Mother of All Attacks) starts at wave 20. GMOAA (Grand Mother) at 45 with increasing frequency. GGMOAA at wave 75. And .unwrap() at wave 100, every 20 waves after that.
The problem was that early waves were boring if late waves were hard, and late waves were trivial if early waves were fun. I ended up keeping HP constant and scaling purely on enemy count and spawn speed. That way early waves teach you positioning while late waves become a DPS check. The 12-wave cycle rotates through enemy types so you can’t just spam one tower and win.
The Grid
The path layout is literally a lava lamp shape:
const LAVA_LAMP_LAYOUT: GridType[][] = [
[0,0,0,0,0,0,3,0,0,0,0,0,0,0], // Top (Spawn)
[0,0,0,0,0,0,1,0,0,0,0,0,0,0],
[0,0,0,0,1,1,1,0,0,0,0,0,0,0],
[0,0,0,1,1,0,0,0,0,1,1,1,1,0],
[0,0,1,1,0,0,0,0,1,1,0,0,1,0],
// ... snakes all the way down
[0,0,0,0,0,0,0,0,0,0,0,2,0,0], // Base
];
3 is the spawn point, 1 is path, 2 is the base (your lava lamps), and 0 is buildable space. Enemies enter at the top and snake through the lava lamp silhouette down to the base. Towers go on the zeros.
For performance, towers don’t scan every enemy every frame. There’s a spatial grid that buckets enemies by position, so towers only check nearby buckets:
const GRID_SIZE = TILE_SIZE * 2;
const enemyBuckets: Enemy[][] = Array.from(
{ length: GRID_COLS * GRID_ROWS }, () => []
);
const collectEnemiesInRange = (x, y, range) => {
const minCol = Math.max(0, Math.floor((x - range) / GRID_SIZE));
const maxCol = Math.min(GRID_COLS - 1, Math.floor((x + range) / GRID_SIZE));
// only check relevant buckets, not all enemies
};
This matters when wave 30+ is dumping 100+ enemies on screen and you have 15 towers all scanning for targets at 60fps.
Stack
React 19, TypeScript, Konva for canvas rendering, Tailwind for UI, Vite for bundling. Deployed on Cloudflare Pages, naturally. The whole thing came together in about two weeks of evening sessions.
Play it here or check out the source on GitHub.