<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: CoinQuiz</title>
    <description>The latest articles on DEV Community by CoinQuiz (@coinquiz).</description>
    <link>https://dev.to/coinquiz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4220454%2F250ad56d-fb30-48e0-9154-53ea1ed85a9a.png</url>
      <title>DEV Community: CoinQuiz</title>
      <link>https://dev.to/coinquiz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coinquiz"/>
    <language>en</language>
    <item>
      <title>How I ran a live multiplayer trivia game on PythonAnywhere's free tier (no WebSockets)</title>
      <dc:creator>CoinQuiz</dc:creator>
      <pubDate>Tue, 03 Nov 2026 17:11:29 +0000</pubDate>
      <link>https://dev.to/coinquiz/how-i-ran-a-live-multiplayer-trivia-game-on-pythonanywheres-free-tier-no-websockets-14el</link>
      <guid>https://dev.to/coinquiz/how-i-ran-a-live-multiplayer-trivia-game-on-pythonanywheres-free-tier-no-websockets-14el</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;CoinQuiz Live&lt;/strong&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Swap the socket for a tiny polling shim
&lt;/h2&gt;

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

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

&lt;p&gt;So the game code doesn't know whether it's talking to a real socket or to HTTP polling.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. No threads? Advance the game lazily
&lt;/h2&gt;

&lt;p&gt;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 &lt;strong&gt;every incoming request first calls &lt;code&gt;tick()&lt;/code&gt;&lt;/strong&gt;, which advances the state machine as many steps as the clock says it should.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Presence instead of disconnect events
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Same events, monkey-patched emit
&lt;/h2&gt;

&lt;p&gt;The server-side game logic calls &lt;code&gt;emit(...)&lt;/code&gt; and &lt;code&gt;sio.emit(...)&lt;/code&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. RTL is more than &lt;code&gt;dir="rtl"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you want to see the polling approach in action, the game is here: &lt;a href="https://coinquiz.pythonanywhere.com" rel="noopener noreferrer"&gt;https://coinquiz.pythonanywhere.com&lt;/a&gt; (guest play, EN/HE). There's also an itch.io page: &lt;a href="https://coinquiz.itch.io/coinquiz-live" rel="noopener noreferrer"&gt;https://coinquiz.itch.io/coinquiz-live&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback on the approach, especially if you've solved "realtime without WebSockets" differently.&lt;/p&gt;

</description>
      <category>python</category>
      <category>gamedev</category>
      <category>showdev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
