Skip to main content

What is RESP?

RESP (Redis Serialization Protocol) is the wire protocol used by Redis and compatible systems like ValKeyper. It’s a simple, human-readable protocol that’s easy to implement and debug.

Parser Implementation

ValKeyper’s RESP parser is implemented in app/resp/parser.go as a wrapper around Go’s bufio.Reader:
type Parser struct {
    *bufio.Reader
}

func NewParser(rdr io.Reader) *Parser {
    return &Parser{
        bufio.NewReader(rdr),
    }
}

RESP Data Types

RESP supports several data types, each indicated by the first byte:
Simple strings start with + and end with \r\n:
+OK\r\n
+PONG\r\n
ValKeyper generates these for simple responses:
case "PING":
    res = []byte("+PONG\r\n")

Length Encoding

RESP uses length-prefixed strings and arrays. ValKeyper parses lengths with:
func (p *Parser) GetLength() (int, error) {
    buff := strings.Builder{}
    for {
        chr, err := p.ReadByte()
        if err != nil {
            return -1, err
        }
        if chr == '\r' {
            p.UnreadByte()
            break
        }
        err = buff.WriteByte(chr)
        if err != nil {
            return -1, err
        }
    }
    res, err := strconv.Atoi(buff.String())
    if err != nil {
        return -1, err
    }
    return res, nil
}
This function:
  1. Reads bytes until it encounters \r (start of \r\n terminator)
  2. Converts the accumulated bytes to an integer
  3. Returns the length for subsequent parsing

Main Parse Function

The main Parse() function dispatches to the appropriate parser based on the type indicator:
func (p *Parser) Parse() ([]string, error) {
    iden, err := p.ReadByte()
    if err != nil {
        return nil, err
    }
    res := []string{}
    switch string(iden) {
    case "*":
        res, err = p.ParseArray()
        if err != nil {
            return nil, err
        }
    case "$": // This case reads rdb after handshake
        buff, err := p.ParseBulkString()
        if err != nil {
            return nil, err
        }
        fmt.Println(string(buff))
    default:
        fmt.Printf("got %s\n", string(iden))
    }
    return res, nil
}
ValKeyper primarily receives arrays of bulk strings from clients (commands and arguments), so the array parser is the most commonly used code path.

Real-World Example

Let’s trace how a SET key value command flows through the parser:
1

Client sends

*3\r\n$3\r\nSET\r\n$3\r\nkey\r\n$5\r\nvalue\r\n
2

Parse() reads '*'

Dispatches to ParseArray()
3

GetLength() reads '3'

Array has 3 elements
4

Parse each element

  • Reads $, calls ParseBulkString()
  • GetLength() returns 3
  • Reads “SET” into result[0]
  • Repeats for “key” and “value”
5

Returns []string

[]string{"SET", "key", "value"}

Response Formatting

ValKeyper uses helper functions to format responses:
// Simple string response
res = []byte("+OK\r\n")

// Error response
res = []byte("-ERR unknown command\r\n")

// Integer response
res = resp.ToInt(42)  // ":42\r\n"

// Bulk string response
res = []byte(resp.ToBulkString("Hello"))  // "$5\r\nHello\r\n"

// Array response
res = resp.ToArray([]string{"key", "value"})  
// "*2\r\n$3\r\nkey\r\n$5\r\nvalue\r\n"

Special Case: RDB Transfer

During replication, the master sends an RDB snapshot as a bulk string:
case "PSYNC":
    res = []byte(fmt.Sprintf("+FULLRESYNC %s %d\r\n", 
        kv.Info.MasterReplId, kv.Info.MasterReplOffSet))

    rdbFile, err := hex.DecodeString("524544495330303131...")
    if err != nil {
        panic(err)
    }

    // Send as bulk string
    tmp := append([]byte(fmt.Sprintf("$%d\r\n", len(rdbFile))), rdbFile...)
    res = append(res, tmp...)
The slave uses ParseBulkString() to receive the RDB data:
if string(byt) == "$" {
    rdbContent, _ := parser.ParseBulkString()
    rdb, err := rdb.NewFromBytes(rdbContent)
    // ... load data from RDB ...
}

Protocol Efficiency

Human Readable

Easy to debug with telnet or netcat

Simple to Parse

No complex state machines or backtracking

Binary Safe

Bulk strings can contain any binary data

Compact

Minimal overhead with length prefixes
You can test RESP directly using telnet:
$ telnet localhost 6379
*1\r\n$4\r\nPING\r\n
+PONG

Mixed-Type Arrays

ValKeyper also supports ToArrayAnyType() for arrays containing different RESP types:
func ToArrayAnyType(arr []string) []byte {
    sb := strings.Builder{}
    tmp := fmt.Sprintf("*%d\r\n", len(arr))
    sb.WriteString(tmp)
    for _, ele := range arr {
        sb.WriteString(ele)  // Elements already formatted
    }
    return []byte(sb.String())
}
This is used for complex responses like XRANGE where each element is itself a formatted array or bulk string.

Build docs developers (and LLMs) love