DEV Community

Cover image for How To Build A Raw TCP Server Where Every Millisecond Counts.
Sk
Sk

Posted on • Edited on • Originally published at Medium

How To Build A Raw TCP Server Where Every Millisecond Counts.

TCP is blazingly fast. 🦀

When every millisecond matters - real-time systems, database engines, caches, brokers, mission-critical software; these systems speak raw TCP.

Because when speed and control are the priority, abstraction is the enemy.

HTTP, while reliable, comes at a cost. It sits on top of TCP and adds overhead: headers, footers, encoding, decoding - latency creeps in.

The Cost of HTTP Overhead

Check out what happens under the hood with an HTTP response:

StatusCode        : 200
StatusDescription : OK
Content           : {72, 101, 108, 108...}
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 12
                    Hello world!
Headers           : {[Content-Length, 12]}
RawContentLength  : 12

Enter fullscreen mode Exit fullscreen mode

HTTP is text-based. It requires parsing, extra processing, and additional steps before data even hits the network.

Now compare that to raw TCP:

const data = Buffer.from("hello")
tcpserverClientConnection.write(data)
Enter fullscreen mode Exit fullscreen mode

No overhead. No extra encoding or decoding. Protocol-less. Just raw data, from point A to B, as fast as possible.

An even faster transport exists: UDP - used in real-time apps (P2P gaming, live video streaming). But UDP trades reliability for speed.

TCP: Three Layers Below HTTP

The deeper you go, the more control you have:

HTTP
└── Encoding/Decoding
    └── Binary/Buffers
        └── TCP
Enter fullscreen mode Exit fullscreen mode

If you want to be more than a CRUD engineer, if you want to build networking systems - understanding TCP is non-negotiable.

TCP: A Practical Introduction

One of the most powerful things about Node.js? It is what you make it.

  • Need a CRUD API? ✅
  • Need a systems engine? ✅
  • Need to extend it with C++? ✅

Your perspective on Node defines what you can build.

Let's start simple: working directly with raw memory.

const data = Buffer.alloc(4)
data.writeInt32BE(7) // Store the number 7 as raw binary
Enter fullscreen mode Exit fullscreen mode

What's happening here?
- Allocate 4 bytes (32 bits) of memory
- Write a 32-bit integer (7) as raw data

This is lower-level JavaScript.
Now, let's go deeper and build a raw TCP server.


Writing a TCP Server in Node.js

A bare-bones TCP server:

// server.js
const net = require("node:net")
const server = net.createServer((c) => {
    c.on("data", (data) => console.log(data))
    c.on("error", (err) => console.log(err))
    c.write("hello world") // Implicitly converts string to a buffer
    c.end()
})
server.listen(3000)
Enter fullscreen mode Exit fullscreen mode

Now, run the server and hit it with curl:

curl http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

🚨 ERROR:

curl: The server committed a protocol violation.
Enter fullscreen mode Exit fullscreen mode

Why? Because we're speaking raw TCP, and curl expects HTTP (headers, status codes, and structured responses).

Let's create a TCP client instead:

//client.js
const net = require("node:net")
const c = net.createConnection({ port: 3000, host: "localhost" })
c.on("data", (data) => console.log(data))
c.write("hello")
Enter fullscreen mode Exit fullscreen mode

Run the server and the client, and here's the output:

<Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 21>
Enter fullscreen mode Exit fullscreen mode

That's raw TCP at work. But we can decode it easily since its only a string:


c.on("data", (data) => {
   console.log(data.toString())  // "hello world"
})

Enter fullscreen mode Exit fullscreen mode

The Power of Raw TCP

So why does this matter? Why should you care?
Because some of the most legendary systems you use daily run on pure TCP:

  • MySQL clients
  • RabbitMQ
  • Redis
  • Neo4j
  • Email clients (SMTP, IMAP, POP3)

These systems don't need HTTP's baggage - no cookies, no headers, no JSON parsing. Just raw, efficient, custom protocols.


Building a Custom Protocol

A protocol is simply an agreement:

"When you send me data, I expect it in this format, or I reject it."

Here's how HTTP enforces its protocol:

HTTP/1.1 200 OK
Content-Length: 12
Hello world!
Enter fullscreen mode Exit fullscreen mode

Let's build our own simple binary protocol on top of raw TCP.


Defining Our Protocol

We'll structure our buffer like this:

buffer = msg length (4 bytes) | msg (variable length) | metadata (variable length)

Enter fullscreen mode Exit fullscreen mode

First 4 bytes → Message length
Next bytes → Message
Remaining bytes → Metadata

Now let's encode a message server-side:

const server = net.createServer((c) => {
    c.on("data", (data) => console.log(data.toString()))
    const data = Buffer.from("hello world") // Encode message
    const metadata = Buffer.from([0x00]) // No metadata
    const len = Buffer.alloc(4) // Allocate 4 bytes for length
    len.writeInt32BE(data.length, 0) // Store message length
    const combinedBuffer = Buffer.concat([len, data, metadata])
    c.write(combinedBuffer) // Send to client
    c.end()
})

Enter fullscreen mode Exit fullscreen mode

Now, on the client side, we'll decode this structured message:

const c = net.createConnection({ port: 3000, host: "localhost" })
c.on("data", (data) => {
    console.log(data)
    const len = data.readInt32BE(0)  // Read message length
    console.log(len) 
    const msg = data.subarray(4, 4 + len)  // Extract message
    console.log(msg.toString())
     const metadata = data.subarray(4 + len) // Extract metadata
    console.log(metadata)
})

c.write("hello")

Enter fullscreen mode Exit fullscreen mode

🚀 And just like that, we've built a structured TCP messaging system.


The Bigger Picture

Believe it or not, this is how real-world networking applications work. In C, this buffer-based approach is the standard for high-performance data exchange.

By going beyond HTTP and working with raw TCP, you unlock the ability to build low-level networking systems.


What's Next?

This article is a tiny snippet from a larger series where we build a message broker for distributed systems from scratch.

You'll learn about:

✅ Long-lived TCP connections
 ✅ Buffer handling
 ✅ Serialization & deserialization
 ✅ Connection management
 ✅ Distributed systems architecture

If you've made it this far, you already think like a systems engineer - and that's rare.

The deeper you go, the more valuable you become.

You can find me on x

see ya!🫡

Top comments (5)

Collapse
 
sfundomhlungu profile image
Sk

Bonus you can turn a TCP socket to HTTP, by simply passing an HTTP formatted protocol:

const CRLF = '\r\n';

  c.write(

        `HTTP/1.1 200 OK${CRLF}` +

        `Content-Length: 11${CRLF}${CRLF}` +

        `Hello world`

      );

Enter fullscreen mode Exit fullscreen mode

Now curl and the browser will understand you

Collapse
 
shekharrr profile image
Shekhar Rajput

For anything you are willing to save even a few millisecond, choosing Node is like using using bicycle to go on a moto gp

Collapse
 
sfundomhlungu profile image
Sk

A pure JavaScript BSON serializer can outperform one built with C++ NAPI. Node.js itself is C++, which is why performance depends on the layer you're using, TCP, is just a thin wrapper around native code (tcp_wrap.cpp).

There's a reason we have tensorflow.js with cpp bindings just as python has C bindings It's perspective and how deep you can go! that's all

Collapse
 
shekharrr profile image
Shekhar Rajput

Check out some benchmarks with different HTTP servers setup on different language. Then you'll get to know, when requests reach >40-50K it's so slow.

Thread Thread
 
sfundomhlungu profile image
Sk
  1. HTTP is not TCP but built on tcp the entire idea of the article
  2. HTTP speak strings, TCP speaks raw bytes/binary entire point of the article so......
  3. TCP !=HTTP the article is about TCP

HTTP:

StatusCode        : 200
StatusDescription : OK
Content           : {72, 101, 108, 108...}
RawContent        : HTTP/1.1 200 OK
                    Content-Length: 12
                    Hello world!
Headers           : {[Content-Length, 12]}
RawContentLength  : 12

Enter fullscreen mode Exit fullscreen mode

TCP:

<Buffer 48 65 6c 6c 6f 20 77 6f 72 6c 64 21>
Enter fullscreen mode Exit fullscreen mode

In conclusion as I did in my comment and article: Node.js is not for CRUD(HTTP) only, its a c++ engine with raw binary capabilities as TCP has shown.

BSON serializer and TCP has nothing to do with CRUD and HTTP. CRUD mindset will be the death of software 🤦🏽‍♀️

         +-----------------+
         |     HTTP        |
         | (Strings, CRUD) |
         +--------+--------+
                  |
                  v
         +-----------------+
         |     TCP         |
         | (Raw Bytes)     |
         +-----------------+
Enter fullscreen mode Exit fullscreen mode