I built CoinQuiz Live, a free browser trivia game where everyone in the lobby gets the same 10 questions at the same time, with a 10-second timer per question. It works in English and Hebrew, and you can play as a guest without signing up. "Coins" in the game are just in-game points for the leaderboard. There's no money, no token and nothing to buy.

The original version was Flask + Flask-SocketIO. Then I wanted to host it for free on PythonAnywhere, and hit the wall: the free tier gives you one WSGI worker, no WebSockets and no background threads. Here's how I kept the game "live" anyway.

1. Swap the socket for a tiny polling shim

The front end was already written against Socket.IO (socket.on(...), socket.emit(...)). Rather than rewrite it, I wrote a ~15-line drop-in replacement for io():

  • emit(event, data) becomes a POST /api/emit with a random per-tab session id.
  • Every ~700 ms the browser calls GET /api/poll?sid=....
  • Both endpoints return a list of [event, data] messages, which the shim dispatches to the same handlers Socket.IO would have called.

So the game code doesn't know whether it's talking to a real socket or to HTTP polling.

2. No threads? Advance the game lazily

With Socket.IO I had a background task that slept between phases (lobby, question, reveal, results). Without threads, there's nothing to wake up. Instead, the server stores the time of the next phase transition, and every incoming request first calls tick(), which advances the state machine as many steps as the clock says it should.

Because every open tab polls every 0.7 s, some request always lands within a fraction of a second of each deadline, and the timer is authoritative on the server, so clients can't cheat by slowing down their clock.

3. Presence instead of disconnect events

Sockets tell you when someone leaves. Polling doesn't. Each session id has a "last seen" timestamp, and anyone not seen for 15 seconds is treated as gone. That drives the "players online" count and the lobby.

4. Same events, monkey-patched emit

The server-side game logic calls emit(...) and sio.emit(...) all over the place. In PA mode those are replaced with functions that append messages to a per-request outbox (a thread-local), and the poll response returns that outbox plus any queued broadcasts. The Socket.IO version still runs unchanged on hosts that support WebSockets.

5. RTL is more than dir="rtl"

Supporting Hebrew taught me a few things: mixed Hebrew/English/number strings need bidi isolation or the punctuation jumps around, layout direction has to flip for the whole page, and every string needs a translation key from day one. I ended up writing a small audit script that fails if any UI string is missing a translation.

What I'd do differently

  • Start with a transport abstraction from day one, not after hosting forced it.
  • Write the test suite early. The project now has a pile of small pytest files (rate limits, stats, migrations, questions), and they caught a lot of regressions.
  • Single worker means all game state lives in memory, so I keep persistent stuff (accounts, scores) in SQLite and keep the round state disposable.

Try it

If you want to see the polling approach in action, the game is here: https://coinquiz.pythonanywhere.com (guest play, EN/HE). There's also an itch.io page: https://coinquiz.itch.io/coinquiz-live

I'd love feedback on the approach, especially if you've solved "realtime without WebSockets" differently.