Why sequential redis.get() in a loop is the most common Redis performance mistake, what RESP looks like on the wire, how pipelining eliminates per-command RTT, and when to use MGET/MSET vs explicit pipelines.
F-7 — Pipelining and the RESP Protocol
Who this module is for: You use Redis in a loop — fetching a list of keys one by one, or updating a batch of records sequentially. Your Redis operations work, but you have not thought about what is happening at the network level. This module explains why that pattern is slow, what the RESP protocol looks like on the wire, and how pipelining eliminates the per-command network round-trip to make batch operations 10–100x faster.
The Network Round-Trip Problem
Every Redis command you issue pays a cost that has nothing to do with Redis itself: the time for bytes to travel from your application to the Redis server and back. This is the Round-Trip Time (RTT).
On a typical cloud setup — application and Redis in the same region, connected via a private network — RTT is 0.5–2ms. On a local machine (Redis and app on the same host via TCP loopback), it is < 0.1ms.
Now consider this pattern — common in codebases everywhere:
const keys = ['user:1', 'user:2', 'user:3', /* ... */ 'user:1000'];
const results = [];
for (const key of keys) {
const value = await redis.get(key);
results.push(value);
}
With 1,000 keys and 1ms RTT, this takes at minimum 1,000 × 1ms = 1 second. Redis itself processes each GET in microseconds. You are spending 99.9% of your time waiting for network round-trips.
This is the problem pipelining solves.
What the RESP Protocol Looks Like
Before diving into pipelining, it helps to understand the protocol Redis uses. RESP (REdis Serialization Protocol) is a text-based protocol designed to be simple to implement and fast to parse.
Every Redis command is serialized as a RESP Array. For SET mykey "hello":
*3\r\n ← Array of 3 elements
$3\r\n ← Bulk string of length 3
SET\r\n
$5\r\n ← Bulk string of length 5
mykey\r\n
$5\r\n ← Bulk string of length 5
hello\r\n
The server's response for OK:
+OK\r\n ← Simple string
For a GET response returning "hello":
$5\r\n ← Bulk string of length 5
hello\r\n
For (nil):
$-1\r\n ← Null bulk string (length -1)
For an integer (e.g., INCR returning 42):
:42\r\n ← Integer
For an error:
-ERR wrong number of arguments for 'get' command\r\n
Why this matters for pipelining: RESP is a streaming protocol. Redis reads commands from a socket buffer, processes them, and writes responses back. It does not require request-response pairing — you can send 100 commands before reading any response, and Redis will process them in order and write 100 responses in order.
What Pipelining Is
Pipelining is the technique of sending multiple commands to Redis in a single network write — without waiting for a response between each command. Redis processes them in order and sends all responses back together.
Without pipelining (naive):
Client → [GET user:1] → Redis
Client ← ["value1"] ← Redis
Client → [GET user:2] → Redis
Client ← ["value2"] ← Redis
... 998 more round-trips ...
With pipelining:
Client → [GET user:1][GET user:2]...[GET user:1000] → Redis
Client ← ["value1"]["value2"]...["value1000"] ← Redis
One network write, one network read, 1,000 results. RTT paid once instead of 1,000 times.
Pipelining in Node.js (ioredis)
import Redis from 'ioredis';
const redis = new Redis();
// Create a pipeline
const pipeline = redis.pipeline();
// Queue commands — nothing is sent yet
pipeline.get('user:1');
pipeline.get('user:2');
pipeline.set('counter', 0);
pipeline.incr('counter');
// Execute all queued commands in one batch
const results = await pipeline.exec();
// results: [[null, "value1"], [null, "value2"], [null, "OK"], [null, 1]]
// Each element is [error, result]
The exec() call sends all queued commands to Redis in a single write and waits for all responses.
Benchmarking the difference:
// Naive sequential: ~1000ms for 1000 keys at 1ms RTT
const start1 = Date.now();
for (let i = 0; i < 1000; i++) {
await redis.get(`user:${i}`);
}
console.log(`Sequential: ${Date.now() - start1}ms`);
// Pipelined: ~2-5ms for 1000 keys (single round-trip)
const start2 = Date.now();
const pipe = redis.pipeline();
for (let i = 0; i < 1000; i++) {
pipe.get(`user:${i}`);
}
await pipe.exec();
console.log(`Pipelined: ${Date.now() - start2}ms`);
Sign in to keep reading
The rest of this module is free — sign in with Google to unlock it and track your progress.
Sign in & RegisterDiscussion
0Join the discussion