<?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: Sahaj Bhatt</title>
    <description>The latest articles on DEV Community by Sahaj Bhatt (@sahaj-b).</description>
    <link>https://dev.to/sahaj-b</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%2F1350886%2F48103f7b-0200-49ab-a337-bb20d1d05161.png</url>
      <title>DEV Community: Sahaj Bhatt</title>
      <link>https://dev.to/sahaj-b</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sahaj-b"/>
    <language>en</language>
    <item>
      <title>Benchmarking Socket.IO Servers</title>
      <dc:creator>Sahaj Bhatt</dc:creator>
      <pubDate>Mon, 19 Jan 2026 15:59:36 +0000</pubDate>
      <link>https://dev.to/sahaj-b/benchmarking-socketio-servers-4n9k</link>
      <guid>https://dev.to/sahaj-b/benchmarking-socketio-servers-4n9k</guid>
      <description>&lt;p&gt;You can create 4 different variations of a &lt;a href="https://socket.io/" rel="noopener noreferrer"&gt;Socket.IO&lt;/a&gt; server with minimal code changes. And trust me you &lt;strong&gt;do NOT&lt;/strong&gt; want to use the default one.&lt;br&gt;&lt;br&gt;
I will be comparing combinations of the &lt;em&gt;runtime&lt;/em&gt; (&lt;code&gt;Bun&lt;/code&gt;, &lt;code&gt;Node&lt;/code&gt;) and the &lt;em&gt;websocket server&lt;/em&gt; (&lt;code&gt;ws&lt;/code&gt;, &lt;code&gt;uWebSockets.js&lt;/code&gt;, &lt;code&gt;bun engine&lt;/code&gt;) to see how they perform under load.  &lt;/p&gt;

&lt;p&gt;&lt;a href="https://socket.io/docs/v4/server-installation/" rel="noopener noreferrer"&gt;Official docs&lt;/a&gt; on using these servers with Socket.IO.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Contenders:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Websocket server&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;node-ws&lt;/td&gt;
&lt;td&gt;Node.js 24.11.1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ws&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;node-uws&lt;/td&gt;
&lt;td&gt;Node.js 24.11.1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;uWebSockets.js&lt;/code&gt; v20.52.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bun-ws&lt;/td&gt;
&lt;td&gt;Bun 1.3.6&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ws&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;bun-native&lt;/td&gt;
&lt;td&gt;Bun 1.3.6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@socket.io/bun-engine&lt;/code&gt; 0.1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/ws" rel="noopener noreferrer"&gt;ws&lt;/a&gt; is the default. It's pure JS. It's reliable. But is it fast? (Spoiler: No).&lt;/p&gt;

&lt;p&gt;The test server is a slightly altered version of the backend of my recent project, &lt;a href="https://github.com/sahaj-b/versus-type" rel="noopener noreferrer"&gt;Versus Type&lt;/a&gt;, a real-time PvP typing game. I just removed the Auth, rate limits, and DB calls.  &lt;/p&gt;

&lt;p&gt;For the load generator, I'm using &lt;a href="https://artillery.io/" rel="noopener noreferrer"&gt;Artillery&lt;/a&gt; with the &lt;code&gt;artillery-engine-socketio-v3&lt;/code&gt; plugin to simulate thousands of concurrent clients connecting via WebSocket and playing the game.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Server:&lt;/strong&gt; AWS Standard B2als v2 (2 vCPUs, 4GB RAM) running Ubuntu 22.04 LTS&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Attacker:&lt;/strong&gt; AWS Standard B4als v2 (4 vCPUs, 8GB RAM) running Ubuntu 22.04 LTS&lt;/p&gt;

&lt;h3&gt;
  
  
  The attack flow:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Artillery spawns 4 virtual users per second.&lt;/li&gt;
&lt;li&gt;Each user hits &lt;code&gt;/api/pvp/matchmake&lt;/code&gt; .&lt;/li&gt;
&lt;li&gt;The server runs a matchmaking algo to return a room ID, grouping players into rooms (max 6).&lt;/li&gt;
&lt;li&gt;Users connects via WebSocket, joins the room, get the game state, like passage.&lt;/li&gt;
&lt;li&gt;Server broadcasts the countdown to start the game, players wait until it reaches 0.&lt;/li&gt;
&lt;li&gt;Users emits keystroke at 60 WPM (1 event/200ms).&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;every&lt;/strong&gt; keystroke, server validates it, updates state, and broadcasts to everyone in the room.&lt;/li&gt;
&lt;li&gt;Users sends a ping event every second for latency tracking.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The passage is long enough to ensure no games end before the benchmark is finished.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a simplified version. The server does much more, like broadcasting system messages and wpm updates every second, etc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://github.com/sahaj-b/socketio-benchmark" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt; including server, client and result data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Results
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Winner:&lt;/strong&gt; &lt;code&gt;Node + uWS&lt;/code&gt; (Blue Line)&lt;br&gt;
It outperformed everyone in every metric except memory usage, where Bun took the lead&lt;/p&gt;

&lt;h3&gt;
  
  
  0-800 Clients
&lt;/h3&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%2Flloyo6hf7hrde8fc0rr2.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%2Flloyo6hf7hrde8fc0rr2.png" alt="0-800 Graph" width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The bun servers have significantly low event loop lag (~0ms) than node servers. &lt;code&gt;node-uws&lt;/code&gt; is most stable tho.&lt;br&gt;&lt;br&gt;
The &lt;code&gt;ws&lt;/code&gt; servers (both bun and node) latency(p95) is creeping up upto 15-20ms. The other two are rock solid ~5ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  800-1,500 Clients
&lt;/h3&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%2Fg6kr9veq70yxulfwml38.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%2Fg6kr9veq70yxulfwml38.png" alt="80-1500 Graph" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-ws&lt;/code&gt; explodes. latency spikes very early(~1k clients), followed by &lt;code&gt;bun-ws&lt;/code&gt; and &lt;code&gt;bun-native&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
same with event loop lag.  &lt;/p&gt;

&lt;p&gt;CPU usage goes to 100% for &lt;code&gt;node-ws&lt;/code&gt; on ~1k clients, &lt;code&gt;bun-ws&lt;/code&gt; ~1.2k clients, &lt;code&gt;bun-native&lt;/code&gt; ~1.3k clients.&lt;br&gt;&lt;br&gt;
&lt;code&gt;node-uws&lt;/code&gt; at just 80% CPU at 1.5k clients. It's rising at the nearly same rate as others tho.  &lt;/p&gt;

&lt;p&gt;The throughput becomes unstable for all except &lt;code&gt;node-uws&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Memory usage is interesting. For some reason, &lt;code&gt;node-uws&lt;/code&gt; one dipped like crazy. Not sure why. It builds back up tho.&lt;br&gt;&lt;br&gt;
The bun servers are using less memory overall. Bun's memory management is impressive.&lt;/p&gt;

&lt;p&gt;Basically &lt;code&gt;node-ws&lt;/code&gt; just can't handle the load. You can see the server metrics missing in some places. Meanwhile &lt;code&gt;node-uws&lt;/code&gt; is just chilling with flat latency and event loop lag.&lt;/p&gt;

&lt;h3&gt;
  
  
  1,500-2,100 Clients
&lt;/h3&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%2F896bqu18mv14e0f72kbw.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%2F896bqu18mv14e0f72kbw.png" alt="1500-2100 Graph" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-ws&lt;/code&gt;, &lt;code&gt;bun-ws&lt;/code&gt;, and &lt;code&gt;bun-native&lt;/code&gt; are all now effectively dead. Latency is through the roof.&lt;br&gt;&lt;br&gt;
It's interesting to see that &lt;code&gt;node-uws&lt;/code&gt; is at constant ~80% CPU usage for the entire range. It's still chilling with low latency.&lt;/p&gt;

&lt;p&gt;Latency p95 of &lt;code&gt;node-ws&lt;/code&gt; stayed constant for some time, lower than &lt;code&gt;bun-native&lt;/code&gt;. This is likely because the metrics didn't get recorded and due to the nature of artillery(pushgateway), it shows the last recorded value until a new one comes in.&lt;/p&gt;

&lt;h3&gt;
  
  
  2,100-3,300 Clients
&lt;/h3&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%2F9z4fc4fmlys0rua4rt4x.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%2F9z4fc4fmlys0rua4rt4x.png" alt="2100-3300 Graph" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-uws&lt;/code&gt; is the only one still standing. It's at ~90-100% CPU now.&lt;br&gt;&lt;br&gt;
Throutput starts to become less stable, and latency slowly creeps up. It goes dead after ~3250 clients.&lt;br&gt;&lt;br&gt;
We can say it could handle a solid 3000-3100 concurrent clients just fine, more than double the next best(&lt;code&gt;bun-native&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Full Graph
&lt;/h3&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%2Fj46ux807ur388inzxs51.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%2Fj46ux807ur388inzxs51.png" alt="Full Graph" width="800" height="919"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CSVs are available &lt;a href="https://github.com/sahaj-b/socketio-benchmark/tree/main/results/csvs" rel="noopener noreferrer"&gt;here on github&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bun, what happened?
&lt;/h2&gt;

&lt;p&gt;It's a surprise to see &lt;code&gt;bun-native&lt;/code&gt; get absolutely destroyed here, because Bun websocket server uses uWebSockets under the hood.&lt;/p&gt;

&lt;p&gt;I don't exactly know the reason why, but it might be because &lt;code&gt;@socket.io/bun-engine&lt;/code&gt; is still very new (v0.1.0) and may have inefficiencies and abstraction layers that add overhead.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>javascript</category>
      <category>node</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
