fivenines

Theory / 10 min

RESP Protocol Parser

Before Redis can be a database, it has to be a careful reader.

Clients do not send JavaScript objects or JSON payloads to Redis. They send bytes. Redis interprets those bytes using RESP, the Redis Serialization Protocol. RESP is simple enough to implement in an afternoon, but it encodes several deep decisions: commands must be binary safe, easy to stream, cheap to parse, and able to carry multiple requests without waiting for replies.

The parser's job is narrow:

input buffer -> RESP value -> consumed byte count

It should not know what GET or SET means. It only turns bytes into structured values.

Arrays Of Bulk Strings

Most Redis commands arrive as RESP arrays whose elements are bulk strings. For example:

*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$3\r\nAda\r\n

This represents:

["SET", "name", "Ada"]

The *3 says the array has three elements. Each $N says the next bulk string has exactly N bytes of payload, followed by \r\n.

This is why RESP is not just line splitting. A bulk string can contain spaces, newlines, or zero bytes. The declared byte length is the contract. Once you trust lengths rather than delimiters inside payloads, Redis commands become binary safe.

Replies Use The Same Family Of Shapes

RESP is also used for replies. A few forms cover most of what a Redis-like server needs:

+OK\r\n                 simple string
-ERR wrong type\r\n     error
:42\r\n                 integer
$3\r\nhey\r\n           bulk string
$-1\r\n                 null bulk string
*2\r\n$3\r\nfoo...      array

This small vocabulary gives Redis a consistent wire format without requiring a heavy serialization layer.

Incomplete Is Not Invalid

TCP is a stream. It does not preserve application message boundaries. A client can send one command in five packets or five commands in one packet. A server that assumes one socket read equals one command is already broken.

The parser therefore needs three outcomes:

type ParseResult =
  | { status: "ok"; value: RespValue; consumed: number }
  | { status: "incomplete" }
  | { status: "error"; message: string };

Consider this input:

*2\r\n$3\r\nGET\r\n$4\r\nna

That is not a protocol error. It is probably the first part of name. The connection should keep the bytes, wait for more data, and resume parsing later.

By contrast, a negative array length where none is allowed, a non-numeric bulk length, or a missing \r\n after a complete payload can be a protocol error. The difference between "not enough bytes yet" and "these bytes can never be valid" is central to robust network software.

Pipelining Falls Out Naturally

RESP makes pipelining almost boring, which is exactly the point.

If a connection's input buffer contains three complete RESP arrays, the server parses the first, dispatches it, removes the consumed bytes, and repeats. The client does not need to wait after each command. Replies are still written in order because commands are processed in order on that connection.

read bytes
  -> parse command A
  -> parse command B
  -> parse command C
  -> queue replies A, B, C

No special "pipeline mode" is required. The protocol and connection buffer already have the right shape.

A Good Parser Is Boring In The Best Way

RESP parsing is not where Redis command semantics belong. It should be deterministic, byte-oriented, and suspicious of malformed frames. The rest of the server can then rely on a clean boundary: once dispatch receives an argv array, it is no longer thinking about packet fragmentation, CRLF detection, or bulk-string lengths.

That separation is small, but it pays rent everywhere.