In the first post, I shared the overview of my Redis clone built from scratch in Node.js.
In this post, I want to go one layer deeper into one of the most important parts of the project:
RESP — the Redis Serialization Protocol.
Before my Redis clone could support commands like:
SET name Alice
GET name
LPUSH queue task1
it first needed to understand what the client was actually sending over the network.
That is where RESP comes in.
Why Redis Needs a Protocol
When we use Redis from a CLI or client library, we usually think in commands:
SET name Alice
But Redis does not internally receive a nice JavaScript array like:
["SET", "name", "Alice"]
It receives raw bytes over a TCP socket.
Those bytes need structure.
The server must know:
- where the command starts
- where the command ends
- how many arguments exist
- how long each argument is
- whether the value is a string, integer, error, array, or bulk string
That structure is provided by RESP.
RESP is the language Redis clients and Redis servers use to talk to each other.
What RESP Looks Like
A command like:
GET name
is represented in RESP as:
*2\r\n$3\r\nGET\r\n$4\r\nname\r\n
At first, this looks strange.
But once broken down, it is simple.
*2
means this is an array with 2 elements.
$3
GET
means the first bulk string has length 3, and its value is GET.
$4
name
means the second bulk string has length 4, and its value is name.
So the server parses this:
*2\r\n$3\r\nGET\r\n$4\r\nname\r\n
into this:
["GET", "name"]
Now the command engine can execute it.
Example: SET Command in RESP
A command like:
SET name Alice
becomes:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nAlice\r\n
Breakdown:
*3
There are 3 parts in the command.
$3
SET
First argument is SET.
$4
name
Second argument is name.
$5
Alice
Third argument is Alice.
The parser converts this into:
["SET", "name", "Alice"]
Then the server can route it to the SET command handler.
RESP Data Types
RESP is not only used for commands.
It is also used for server responses.
Some common RESP types are:
+ Simple Strings
- Errors
: Integers
$ Bulk Strings
* Arrays
For example, when Redis replies with OK:
+OK\r\n
When a key does not exist:
$-1\r\n
When returning an integer:
:1\r\n
When returning an error:
-ERR unknown command\r\n
When returning an array:
*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n
This is why a Redis clone needs both:
RESP parser
RESP encoder
The parser reads client commands.
The encoder sends Redis-compatible responses back.
Why Not Just Split by Spaces?
At the beginning, it is tempting to parse commands like this:
const parts = input.trim().split(" ");
For very simple commands, this might work.
Example:
GET name
But it breaks quickly.
What about values with spaces?
SET message "hello world"
What about binary-safe strings?
What about multiple commands arriving together?
What about half a command arriving in one TCP chunk and the rest arriving later?
Redis needs a protocol that can handle all of this reliably.
RESP solves this by including length information.
Instead of guessing where a value ends, the server reads exactly the number of bytes specified.
For example:
$11
hello world
The server knows the value is exactly 11 bytes long.
That makes RESP binary-safe and much more reliable than splitting strings by spaces.
The Hard Part: TCP Is a Stream
The biggest lesson while implementing RESP was this:
TCP is stream-based, not message-based.
That means the server does not always receive one complete command at a time.
A client might send:
*2\r\n$3\r\nGET\r\n$4\r\nname\r\n
But the server may receive it in pieces.
For example, first chunk:
*2\r\n$3\r\nGE
Second chunk:
T\r\n$4\r\nname\r\n
Or the opposite can happen.
Multiple commands may arrive in a single chunk:
*1\r\n$4\r\nPING\r\n*2\r\n$3\r\nGET\r\n$4\r\nname\r\n
So the parser cannot assume:
one socket data event = one command
That assumption is wrong.
Instead, the parser needs to maintain an internal buffer.
How Incremental Parsing Works
In my Redis clone, the RESP parser works incrementally.
The idea is:
1. Receive a chunk from the socket
2. Add it to a buffer
3. Try to parse a complete command
4. If the command is incomplete, wait for more data
5. If one command is complete, return it
6. If more data remains, keep parsing
The parser has to be patient.
If the buffer does not contain enough data yet, it should not throw an error immediately.
It should simply wait for the next chunk.
For example:
*2\r\n$3\r\nGET\r\n$4\r\nna
This is not invalid.
It is just incomplete.
The parser should remember it and continue when the next chunk arrives:
me\r\n
Then it can produce:
["GET", "name"]
This was one of the first moments where the project felt like real systems engineering.
The problem was not just parsing strings.
The problem was parsing an ongoing network stream safely.
Parser Flow
At a high level, the parser does this:
Socket receives bytes
↓
Append bytes to parser buffer
↓
Check first RESP marker
↓
Parse array length
↓
Parse each bulk string
↓
If complete, emit command
↓
Remove parsed bytes from buffer
↓
Keep remaining bytes for next parse
For a command like:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nAlice\r\n
the parser needs to read:
array length = 3
bulk string length = 3
value = SET
bulk string length = 4
value = name
bulk string length = 5
value = Alice
Only then can it emit:
["SET", "name", "Alice"]
How This Fits Into the Redis Clone
Once the RESP parser produces a command array, the rest of the server can work with normal JavaScript data.
The flow becomes:
Raw TCP bytes
↓
RESP parser
↓
Command array
↓
Command router
↓
Command handler
↓
Storage / persistence / replication
↓
RESP response
↓
Socket write
So when the client sends:
SET name Alice
the internal flow is:
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nAlice\r\n
↓
["SET", "name", "Alice"]
↓
SET handler
↓
store name = Alice
↓
append to AOF
↓
update snapshot
↓
propagate to replicas
↓
+OK\r\n
This is what made me appreciate Redis more.
A simple command goes through many layers before the user sees OK.
RESP Responses
Parsing client commands is only half of the protocol.
The server also needs to send valid RESP responses.
For example:
For PING:
+PONG\r\n
For successful SET:
+OK\r\n
For GET name when the value is Alice:
$5\r\nAlice\r\n
For a missing key:
$-1\r\n
For DEL name returning one deleted key:
:1\r\n
For errors:
-ERR wrong number of arguments\r\n
This matters because Redis clients expect protocol-compatible responses.
If the response format is wrong, the client cannot understand the server.
So the clone needed RESP encoding helpers, not just command logic.
Things I Learned While Building the Parser
1. Protocol design removes ambiguity
Without RESP, parsing commands with spaces, quotes, or binary data becomes messy.
With RESP, the server knows exactly how many bytes to read.
2. TCP does not care about your command boundaries
TCP gives you a stream of bytes.
It does not promise that each command arrives separately.
The application protocol has to define message boundaries.
3. A parser needs state
The parser must remember incomplete data between chunks.
That buffer is what makes incremental parsing possible.
4. Good parsing keeps the rest of the system clean
Once the parser returns:
["SET", "name", "Alice"]
the command handlers do not need to care about raw TCP bytes.
That separation makes the server easier to build.
5. RESP is simple but powerful
RESP is readable enough to debug manually, but structured enough to support real clients and binary-safe values.
That is a great balance.
Why This Part Matters
It is easy to think the database starts at the storage layer.
But actually, the database starts at the protocol layer.
Before Redis can store a key, it has to understand the command.
Before it can return a value, it has to encode the response.
Before it can support clients reliably, it has to handle partial messages, multiple messages, and malformed input.
That is why implementing RESP was one of the most important parts of this project.
It turned a simple TCP server into something Redis-compatible.
Final Thought
Before this project, I saw Redis commands as text commands:
SET key value
GET key
After implementing RESP, I started seeing them as structured messages flowing over TCP.
That shift changed how I think about backend systems.
A protocol is not just a format.
It is the contract between two systems.
And in Redis, RESP is that contract.
In the next post, I’ll go deeper into the in-memory storage layer: how strings, lists, hashes, and expiry are represented internally.
Top comments (0)