Data · August 9, 2026 · 5 min read

Missing Is Not Zero

Game Combiner warns you when a multiplayer game's community has left. Improving that warning produced a version that flagged Overwatch 2 as dead while 61,499 people were in game. The bug is one of the oldest in data work and it still got us.

Recommending an online-only game whose servers are empty is a bad experience. So each game carries a dead_mp flag: multiplayer, not single-player, almost nobody playing, enough reviews that the silence means something. The flag shows a warning and applies a small scoring penalty.

It was firing on 866 games out of 142,000. That seemed low.

Why it barely worked

The flag needs a player count, and our bulk dataset carried one for only 19,689 of 136,712 games. The other 86% sat at zero — not because nobody played them, but because nobody had told us. Games with no player data mostly weren't detected as multiplayer either, so they quietly fell out of the calculation instead of being flagged wrongly. Broken, but harmlessly.

Then we scraped real tags for 36,971 games. Suddenly thousands more games were correctly identified as multiplayer — and they all still had a stored player count of zero.

The result

Recomputing the flag produced 3,288 dead games, up from 866. That looked like a fix. Sorting by review count to sanity-check the biggest ones did not:

Overwatch 2                      415,947 reviews   ccu=0   FLAGGED DEAD
ARC Raiders                      411,327 reviews   ccu=0   FLAGGED DEAD
PEAK                             342,622 reviews   ccu=0   FLAGGED DEAD
THE FINALS                       273,872 reviews   ccu=0   FLAGGED DEAD
Borderlands 4                     82,260 reviews   ccu=0   FLAGGED DEAD

Steam's own API, asked directly, reported 61,499 people playing Overwatch 2 right then.

The code said (ccu or 0) < DEAD_CCU. A missing value became zero, zero is less than the threshold, so the game was declared dead. The database couldn't distinguish "we measured this and nobody is playing" from "we never asked."

The other trap, one layer down

Picking a data source had its own version of the same mistake. SteamSpy exposes a ccu field, and it looked authoritative: for Rust it returned 143,870, matching our stored value exactly. Identical to the digit. That felt like proof.

It was a coincidence of an old, popular, long-stable title. For Overwatch 2, SteamSpy reported ccu=0 and owners: 0..20,000 — confidently, in a well-formed response, while tens of thousands of people were in game.

A single matching data point is not validation. It's one matching data point. We switched to Steam's own GetNumberOfCurrentPlayers, which is authoritative because it's Valve reporting on Valve's own servers.

What actually fixed it

Not a better threshold — a different contract. The recompute now takes a map of games actually measured in this run:

live = measured.get(appid)
if live is None:
    continue        # unmeasured: leave the flag alone, never assume dead

A failed API call now means "we don't know," which is the truth. We polled all 10,542 eligible multiplayer games directly and found 2,769 genuinely empty ones:

FLAGGED     Z1 Battle Royale  6 players    Warface: Clutch 6    Blackwake 2
NOT FLAGGED CS2 642,068       PUBG 344,375  Dota 2 474,699      Overwatch 2 61,499

One nice confirmation that the fail-safe works: Apex Legends' lookup didn't return a valid result, so it went unmeasured — and stayed unflagged rather than being guessed at.

The bit that generalises

This class of bug survives because the wrong behaviour is indistinguishable from the right one until you look at specific rows. The flag count went from 866 to 3,288 and that felt like progress. Only checking which games were flagged, against the world, exposed it.

Then it came back. Wiring the same job into weekly automation, we capped the sweep to keep the run bounded — and the recompute cleared flags for every game it hadn't re-checked. Same mistake, new location: unmeasured being treated as measured-and-fine. The capped run would have silently wiped all 2,769 flags down to whatever it happened to look at that week.

If your schema can't tell absent from zero, some code path will eventually conflate them, and it'll happen most easily right after you improve something else.