<?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: Erol</title>
    <description>The latest articles on DEV Community by Erol (@erol4).</description>
    <link>https://dev.to/erol4</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F351309%2Fbad8607c-bdc6-49c9-9cca-b655fffeb0a8.jpeg</url>
      <title>DEV Community: Erol</title>
      <link>https://dev.to/erol4</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/erol4"/>
    <language>en</language>
    <item>
      <title>How I Sync Real-Time Multiplayer Game State with Socket.io and Node.js</title>
      <dc:creator>Erol</dc:creator>
      <pubDate>Wed, 01 Apr 2026 13:46:23 +0000</pubDate>
      <link>https://dev.to/erol4/how-i-sync-real-time-multiplayer-game-state-with-socketio-and-nodejs-1d8c</link>
      <guid>https://dev.to/erol4/how-i-sync-real-time-multiplayer-game-state-with-socketio-and-nodejs-1d8c</guid>
      <description>&lt;p&gt;I built a multiplayer board game that runs in the browser. Keeping everyone's screen in sync was the hardest part by far.&lt;/p&gt;

&lt;p&gt;Here's how I did it in &lt;a href="https://tilelord.com" rel="noopener noreferrer"&gt;TileLord&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server owns everything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Never trust the client.&lt;/strong&gt; The client sends what the player wants to do. The server checks if it's valid, updates the game, and tells everyone what happened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Client&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placeTile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Server&lt;/span&gt;
&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;placeTile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryPlaceTile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tilePlaced&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;actionRejected&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even for a chill board game, people will try to mess with the payloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  One room per game
&lt;/h2&gt;

&lt;p&gt;Each game gets its own Socket.io room. Players join when they enter, and updates only go to that room.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`game:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`game:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gameStateUpdate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sanitizedState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't send everything though.&lt;/strong&gt; If your game has hidden info (like a tile deck), strip that out before sending. Each player should only see what they're allowed to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log every action
&lt;/h2&gt;

&lt;p&gt;I store every move as an event instead of just keeping the current state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;actionLog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TILE_PLACED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;rotation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get a lot from this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Replays&lt;/strong&gt; - play the log forward&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconnects&lt;/strong&gt; - rebuild what a player missed while offline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugging&lt;/strong&gt; - replay the exact sequence that caused a bug&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The replay feature took about a day to build and players use it all the time. Worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disconnects are normal
&lt;/h2&gt;

&lt;p&gt;People lose wifi. Their phone locks. Their laptop sleeps. Handle this from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn timeouts:&lt;/strong&gt; if someone doesn't move in time, a bot takes their turn so the game keeps going.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;turnTimer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;botPlayer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;takeTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;botMove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;TURN_TIMEOUT_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reconnects:&lt;/strong&gt; when they come back, send them the current state.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rejoinGame&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;playerId&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`game:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;roomId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fullState&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;room&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;game&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getStateForPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Spectators are free
&lt;/h2&gt;

&lt;p&gt;Once you have rooms, spectators just join without being able to send game actions. They get the same updates as players.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL/DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Server decides everything. Clients just ask.&lt;/li&gt;
&lt;li&gt;Socket.io rooms, one per game.&lt;/li&gt;
&lt;li&gt;Log every action. Replays and debugging come for free.&lt;/li&gt;
&lt;li&gt;Handle disconnects early. They will happen constantly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Get the sync layer right first. The rest falls into place after that.&lt;/p&gt;




&lt;p&gt;I'm building TileLord at &lt;a href="https://tilelord.com" rel="noopener noreferrer"&gt;tilelord.com&lt;/a&gt;, a free Carcassonne alternative you can play with friends or bots online.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>node</category>
      <category>gamedev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Why Letting Piscina Spin Up Workers Dynamically Broke My Game Server</title>
      <dc:creator>Erol</dc:creator>
      <pubDate>Sun, 08 Feb 2026 08:07:30 +0000</pubDate>
      <link>https://dev.to/erol4/why-letting-piscina-spin-up-workers-dynamically-broke-my-game-server-359e</link>
      <guid>https://dev.to/erol4/why-letting-piscina-spin-up-workers-dynamically-broke-my-game-server-359e</guid>
      <description>&lt;p&gt;First, the background: I'm running a Carcassonne alternative web game called &lt;a href="https://tilelord.com/" rel="noopener noreferrer"&gt;TileLord&lt;/a&gt;. It also supports singleplayer mode, so you play against bots.&lt;/p&gt;

&lt;p&gt;Bots are CPU-heavy, as they calculate heuristics for each possible move the bot could make, so an obvious way is to use threads on the server side, otherwise, your whole server will "lag" each time a bot is calculating a move (which happened when I first launched the game).&lt;/p&gt;

&lt;h2&gt;
  
  
  Worker threads and Piscina
&lt;/h2&gt;

&lt;p&gt;As mentioned, it was obvious to add threads, and chatgpt suggested that I use &lt;a href="https://github.com/piscinajs/piscina" rel="noopener noreferrer"&gt;Piscina&lt;/a&gt;. On Github, this is their repo description:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A fast, efficient Node.js Worker Thread Pool implementation&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So it makes total sense to use it. Gpt suggested using this configuration, based on the VPS I was running my server on (8 cores):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;minThreads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxThreads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I rebooted the server, and.. the lag was gone! It worked perfectly. Until... my game got more users, and bots started getting slow-ish again. 3 seconds for a bot move instead of 1.5s.&lt;/p&gt;

&lt;p&gt;I ran &lt;code&gt;htop&lt;/code&gt; on my server, and saw random spikes of 800% cpu utilization (all 8 cores at 100%) for a few seconds, then back to 0%. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78pz9bkkhfn51yefif6x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F78pz9bkkhfn51yefif6x.png" alt="htop showing all 8 cores hitting 100% utilization" width="800" height="249"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Which doesn't make any sense. There should be max 6 worker threads, and nothing else should consume much cpu, right?&lt;/p&gt;

&lt;p&gt;Wrong! Spawning a worker in very heavy in Nodejs, as it needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new v8 isolate&lt;/li&gt;
&lt;li&gt;Parse + execute your worker's js bundle&lt;/li&gt;
&lt;li&gt;Run module initialization (&lt;code&gt;require&lt;/code&gt;/&lt;code&gt;import&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;JIT-compile hot code paths&lt;/li&gt;
&lt;li&gt;Maybe it triggers garbage collector activity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And these operations are parallelized internally, so when Piscina is spawning a new worker it &lt;em&gt;can&lt;/em&gt; hit all cpu cores at once, causing the whole server to lag for a second or two.&lt;/p&gt;

&lt;p&gt;After hard-coding both min/max threads to 6 (so the other 2 cpu cores are available to the server), my cpu consumption didn't go above 200%  (I added the fix 4.2): &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe0wh784pqka9uszvtth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffe0wh784pqka9uszvtth.png" alt="Cpu compute graph over last week" width="620" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Lesson learned
&lt;/h2&gt;

&lt;p&gt;Worker creation can be far more expensive than actual worker execution (bot move compute, which atm is 50-500ms). In such case, it might make sense to pre-allocate threads:)&lt;/p&gt;

&lt;p&gt;Sometimes the bug isn’t in your algorithm, it’s in how your workers come into existence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read more
&lt;/h2&gt;

&lt;p&gt;I also wrote about how &lt;a href="https://dev.to/erol4/how-i-built-a-free-online-carcassonne-game-alt-you-can-play-in-the-browser-2mgd"&gt;How I Built a Free Online Carcassonne Game Alt You Can Play in the Browser&lt;/a&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>How I Built a Free Online Carcassonne Game Alt You Can Play in the Browser</title>
      <dc:creator>Erol</dc:creator>
      <pubDate>Thu, 09 Oct 2025 10:44:41 +0000</pubDate>
      <link>https://dev.to/erol4/how-i-built-a-free-online-carcassonne-game-alt-you-can-play-in-the-browser-2mgd</link>
      <guid>https://dev.to/erol4/how-i-built-a-free-online-carcassonne-game-alt-you-can-play-in-the-browser-2mgd</guid>
      <description>&lt;p&gt;I’ve always liked the board game Carcassonne — simple mechanics, but surprisingly deep strategy.&lt;br&gt;
A while back I started tinkering with the idea of building something similar that could be played easily in a browser with friends. That side project eventually turned into &lt;a href="https://tilelord.com" rel="noopener noreferrer"&gt;TileLord&lt;/a&gt; — a small web game where you can &lt;strong&gt;play Carcassonne online free&lt;/strong&gt; with bots or friends, no installs needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F71h6aky13jr7j4c69946.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F71h6aky13jr7j4c69946.png" alt="Gameplay" width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I wanted to build it
&lt;/h2&gt;

&lt;p&gt;There aren't many online versions, it's either old Desktop apps or pay-up-front official apps. So I started building an open, Carcassonne game alternative that follows the same core rules&lt;/p&gt;

&lt;p&gt;It’s not the official game (and isn’t affiliated with it), but it scratches the same strategic itch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: React + &lt;a href="https://pixijs.com/" rel="noopener noreferrer"&gt;PixiJS&lt;/a&gt; for tile rendering and UI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Node.js + &lt;a href="https://socket.io/" rel="noopener noreferrer"&gt;Socket.io&lt;/a&gt; for multiplayer state sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting&lt;/strong&gt;: Cloudflare, curretnly still on free tier:)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Hard things
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Extending game engine
&lt;/h3&gt;

&lt;p&gt;The base rules are simple, but once you start adding expansions (like Inns &amp;amp; Cathedrals, River, Abbots, Farmers, etc.), the logic tree gets messy.&lt;/p&gt;

&lt;p&gt;Each feature can affect multiple parts of the scoring or tile-placement rules. I ended up refactoring the entire engine 3 times to add abstraction layers one on top of each other (eg. multi-tile buildings instead of per-tile features).&lt;/p&gt;

&lt;h3&gt;
  
  
  AI Bot heuristics
&lt;/h3&gt;

&lt;p&gt;Deciding where to place a tile, and where to place a follower, isn't as simple as eg. tic-tac-toe solver. You have to account for how many followers you still have, how many open edges of a building, defensibility of your buildings, trying to merge (attack) with other larger buildings, trying to "trap" enemies so they can't complete their cities, calculate what's the chance required tile will be drawn, etc. Many rules/weights that need to be tuned, and additional complexity arises when you add expansions into the base game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bot optimizations
&lt;/h3&gt;

&lt;p&gt;At first it just brute-forced every legal move, deep-copying the full board to estimate the diff in value (which can be score, trapped follower, etc) for each move. But after &amp;gt;50 placed tiles, such algo takes quite some time, up to a point where you are waiting 20+ seconds for a bot move. &lt;/p&gt;

&lt;p&gt;By adding just a state diff and rollbacks (placing/removing followers or tiles), bots required an order of magnitude less compute time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2uz9pelizvx0id19c04t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2uz9pelizvx0id19c04t.png" alt="Big board, replay mode" width="800" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Current state &amp;amp; whats next
&lt;/h2&gt;

&lt;p&gt;Right now, TileLord supports all the base rules plus several expansions. Multiplayer, Replay mode, AI bots, and mobile browser play are all working.&lt;/p&gt;

&lt;p&gt;I plan to add additional expansions (major and mini ones), beginner tutorials, game/turn analysis, better difficulty tuning for AI, and more!&lt;/p&gt;

&lt;p&gt;PS. We're &lt;a href="https://dev.to/erol4/looking-for-senior-fullstack-developer-12eo"&gt;hiring&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>sideprojects</category>
      <category>startup</category>
      <category>webdev</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Looking for Senior Fullstack Developer</title>
      <dc:creator>Erol</dc:creator>
      <pubDate>Fri, 26 Sep 2025 10:04:07 +0000</pubDate>
      <link>https://dev.to/erol4/looking-for-senior-fullstack-developer-12eo</link>
      <guid>https://dev.to/erol4/looking-for-senior-fullstack-developer-12eo</guid>
      <description>&lt;p&gt;About Us&lt;br&gt;
At &lt;a href="https://tilelord.com" rel="noopener noreferrer"&gt;TileLord&lt;/a&gt; we’re building a free, web-based alternative to Carcassonne. We’re a small startup with a fast-paced culture and ambitious goals.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmtctpvzcmmad7vuk8gs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftmtctpvzcmmad7vuk8gs.png" alt=" " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;br&gt;
We’re looking for a Senior Fullstack Developer (remote, CET timezone) with 5+ years of experience. You’ll work across frontend, backend, and database, with a big focus on PIXI.js for gameplay. Game dev experience is a plus.&lt;/p&gt;

&lt;p&gt;Requirements&lt;br&gt;
    • 5+ years fullstack experience&lt;br&gt;
    • Strong FE/BE/DB skills&lt;br&gt;
    • Proficiency in TypeScript&lt;br&gt;
    • PIXI.js or similar rendering/game framework experience&lt;br&gt;
    • Remote-friendly, CET working hours&lt;/p&gt;

&lt;p&gt;Why Join&lt;br&gt;
    • Startup culture with real ownership&lt;br&gt;
    • Fully remote&lt;br&gt;
    • Competitive pay + possible equity&lt;br&gt;
    • Build something fun and free for the gaming community&lt;/p&gt;

&lt;p&gt;Apply&lt;br&gt;
Send your CV + GitHub link to &lt;a href="mailto:erol@tilelord.com"&gt;erol@tilelord.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>hiring</category>
      <category>career</category>
    </item>
  </channel>
</rss>
