A Redis pipeline batches multiple commands into a single network round-trip. The client sends N commands in one write, the server processes them in order, and the client reads N responses in one read. The performance gain comes from collapsing N network round-trips into 1, which is usually 5-10x faster than the same commands sent one at a time. A pipeline is not a transaction. There is no atomicity (another client can interleave), no rollback (commands cannot be undone), and no isolation (commands are visible to other clients as they execute). The right mental model is a network optimization, not a database feature.
The reason “redis pipeline” is still a top search is that the docs treat it as a feature, and developers confuse it with MULTI/EXEC (which is the actual transaction). The two are different tools, and the right answer for most “I need to do 1000 writes fast” workloads is the pipeline, not the transaction.
This post is the pipeline, the transaction, the comparison, and the right pattern for the most common use case.
Table of contents
- The direct answer: a network optimization, not a transaction
- The performance model: N round-trips vs 1
- The pipeline API in the common clients
- The transaction API (
MULTI/EXEC): when atomicity matters - The mixed pattern: pipeline + transaction when both matter
- The failure modes: when the pipeline does not save you
- The right sizing: how many commands per pipeline
- FAQ
The direct answer: a network optimization, not a transaction
A Redis pipeline batches N commands into a single network round-trip. The server still processes the commands in order, one at a time, and the responses come back in the same order. There is no atomicity, no isolation, and no rollback.
The shape:
Client Server
| --- INCR counter -----------> |
| --- GET counter ------------> | (4 round-trips, sequential)
| --- SET counter 100 -------> |
| --- GET counter ------------> |
| <--- 1 -------------------- |
| <--- 100 ------------------ |
| <--- OK -------------------- |
| <--- 100 ------------------ |
vs.
Client Server
| --- INCR counter ---------> |
| --- GET counter ----------> | (1 round-trip, batched)
| --- SET counter 100 ------> |
| --- GET counter ----------> |
| <--- 1 ------------------- |
| <--- 100 ----------------- |
| <--- OK ------------------ |
| <--- 100 ----------------- |
The first version is four round-trips; the second is one. The server still does four operations, but the network cost collapses. For a workload on localhost, the difference is small (sub-millisecond). For a workload across a network, the difference is large (a few milliseconds per round-trip multiplied by 4 or 100 or 10000).
The performance gain is real, and it scales with the batch size. A pipeline of 1000 commands on a remote Redis is 10-50x faster than 1000 individual commands. The server’s CPU cost is roughly the same; the client’s CPU cost is the same; only the network cost changes.
The performance model: N round-trips vs 1
The performance math, in numbers:
- Local Redis (loopback): ~0.1ms per round-trip. A pipeline of 100 commands saves 9.9ms. The speedup is real but not dramatic.
- Same-region cloud Redis: ~1ms per round-trip. A pipeline of 100 commands saves 99ms. The speedup is 10x.
- Cross-region Redis: ~50ms per round-trip. A pipeline of 100 commands saves 4.95s. The speedup is dramatic.
The rule of thumb: the network latency is the floor, and the pipeline eliminates N-1 round-trips of that floor. For a low-latency workload on a high-latency link, the pipeline is the difference between “fast” and “unusable.”
The cost the pipeline does not eliminate: the server’s CPU time. A pipeline of 1000 commands takes roughly the same server CPU as 1000 individual commands. The pipeline is a network optimization, not a CPU optimization. For a CPU-bound workload, the answer is a faster Redis, not a bigger pipeline.
The pipeline API in the common clients
The pipeline API in each of the common Redis clients:
Node (ioredis):
const Redis = require("ioredis");
const redis = new Redis();
const pipeline = redis.pipeline();
pipeline.incr("counter");
pipeline.get("counter");
pipeline.set("counter", 100);
pipeline.get("counter");
const results = await pipeline.exec();
// results = [[null, 1], [null, '1'], [null, 'OK'], [null, '100']]
Node (node-redis):
const { createClient } = require("redis");
const client = createClient();
await client.connect();
const results = await client
.multi()
.incr("counter")
.get("counter")
.set("counter", 100)
.get("counter")
.exec();
(Note: node-redis calls the pipeline multi, which is confusing because MULTI/EXEC in Redis is the transaction. The multi in node-redis is a pipeline, not a transaction. The transaction is client.multi()...exec() in some versions, or a separate client.transaction() method in others. Check the docs.)
Python (redis-py):
import redis
r = redis.Redis()
pipe = r.pipeline()
pipe.incr("counter")
pipe.get("counter")
pipe.set("counter", 100)
pipe.get("counter")
results = pipe.execute()
# results = [1, b'1', True, b'100']
Go (go-redis):
pipe := rdb.Pipeline()
incrCmd := pipe.Incr(ctx, "counter")
getCmd := pipe.Get(ctx, "counter")
setCmd := pipe.Set(ctx, "counter", 100, 0)
getCmd2 := pipe.Get(ctx, "counter")
_, err := pipe.Exec(ctx)
All four clients support the pipeline pattern, and the shape is the same: build a pipeline, add commands, execute, get results in order.
The transaction API (MULTI/EXEC): when atomicity matters
The Redis transaction is MULTI / EXEC. The shape:
MULTI
INCR counter
GET counter
SET counter 100
GET counter
EXEC
The difference from a pipeline: the MULTI/EXEC block is atomic. No other client’s commands can run between the MULTI and the EXEC. The commands in the block are queued, and the server executes them in order, with no interleaving.
The trade-off: the transaction is slower than a pipeline (the server has to queue the commands, check for the EXEC, and execute the whole block atomically). For a workload that does not need atomicity, the pipeline is faster. For a workload that does, the transaction is the right answer.
The common case for transactions: a check-and-set. Read a value, decide what to write, write the new value, all without another client changing the value in between. The pipeline does not give this guarantee; the transaction does.
The other common case: a multi-key update that has to succeed or fail together. A pipeline can fail halfway through; a transaction does not (within the limits of Redis’s transaction model — there is no rollback, but the block is atomic from the perspective of other clients).
The mixed pattern: pipeline + transaction when both matter
For a workload that needs both batching and atomicity, the right answer is a pipeline inside a transaction. The shape:
MULTI
INCR counter
GET counter
SET counter 100
GET counter
EXEC
This is the MULTI/EXEC block from the previous section. The commands inside the block are atomic from the perspective of other clients. The performance is roughly the same as a pipeline (one network round-trip, one server-side block execution).
In code:
pipe = r.pipeline(transaction=True)
pipe.incr("counter")
pipe.get("counter")
pipe.set("counter", 100)
pipe.get("counter")
results = pipe.execute()
The transaction=True flag tells redis-py to wrap the pipeline in a MULTI/EXEC block. The performance is slightly worse than a non-transactional pipeline, but the atomicity guarantee is real.
The decision tree:
- Just want to batch commands for performance? Use a plain pipeline.
- Need atomicity (other clients cannot interleave)? Use a transaction (
MULTI/EXEC), or a pipeline withtransaction=True. - Need to rollback a partial failure? Redis does not have rollback. The application has to compensate. For most workloads, this is the right answer; for some, the right answer is a different database.
The failure modes: when the pipeline does not save you
The pipeline does not save you from these:
- The server crashes mid-pipeline. The commands already executed are committed; the commands not yet executed are lost. The application has to handle the partial state.
- A single command fails (e.g.,
SETon a read-only replica). The other commands in the pipeline still execute. The application has to check each result and decide what to do. - The pipeline is too large. A pipeline of 10 million commands exceeds the server’s input buffer, and the connection is closed. The application has to size the pipeline.
- The pipeline holds a connection. A pipeline reserves a connection for the duration of the batch. A pipeline of 10000 commands holds a connection for 10000 commands. For a multi-tenant Redis, the right answer is to bound the pipeline size.
The pipeline is a network optimization, not a reliability feature. The application is still responsible for handling the partial state, the failures, and the connection management.
The right sizing: how many commands per pipeline
The right pipeline size depends on the workload. The trade-offs:
- Small pipeline (10-100 commands): Low memory usage, low latency per pipeline, more pipelines per second. The right answer for a real-time workload that has to interleave with other operations.
- Medium pipeline (100-1000 commands): Good throughput, moderate memory usage, moderate latency per pipeline. The right answer for a batch processing workload.
- Large pipeline (1000+ commands): Maximum throughput, high memory usage, high latency per pipeline. The right answer for a one-shot bulk load, not a long-running service.
The right number for most workloads: 100-500 commands per pipeline. The throughput gain is near-maximum, the memory usage is reasonable, and the latency per pipeline is short enough to feel responsive.
For a workload where the per-command latency matters more than the throughput (an interactive app, a chat backend), the right answer is a smaller pipeline (10-50 commands) or no pipeline at all. The pipeline’s value is in the round-trip elimination, not in some magic performance boost.
How this fits the rest of the stack
Pipelining is also a hosting cost lever — fewer round-trips means lower CPU per request, which means a smaller instance can handle the same load. The team’s mental model for the Redis cost is the instance size, the bandwidth, the storage, and the request rate. The RunxBuild hosting calculator is the right place to model that — pick the Redis tier, the request rate, the pipeline batch size, and the bandwidth, and the calculator shows what the Redis instance costs before and after the pipeline optimization.
Useful related references:
FAQ
What is a Redis pipeline?
A Redis pipeline batches multiple commands into a single network round-trip. The client sends N commands in one write, the server processes them in order, and the client reads N responses in one read. The performance gain comes from collapsing N round-trips into 1.
Is a pipeline a transaction?
No. A pipeline has no atomicity, no isolation, and no rollback. A transaction (MULTI/EXEC) is the atomic primitive. The two are different tools. For atomicity, use a transaction; for batching, use a pipeline.
How much faster is a pipeline?
The speedup is roughly proportional to the network latency times the batch size minus one. For a local Redis, the speedup is small. For a remote Redis, the speedup is 5-50x. The server’s CPU cost is the same; the network cost is collapsed.
What is the right pipeline size?
For most workloads, 100-500 commands per pipeline. The throughput gain is near-maximum, the memory usage is reasonable, and the latency per pipeline is short enough to feel responsive. Larger pipelines are for one-shot bulk loads, not for long-running services.
How do I pipeline in redis-py?
pipe = r.pipeline() creates a pipeline, pipe.command(args) adds a command, pipe.execute() runs the pipeline and returns the results. Add transaction=True to wrap the pipeline in a MULTI/EXEC block.
How do I pipeline in ioredis?
const pipeline = redis.pipeline() creates a pipeline, pipeline.command(args) adds a command, await pipeline.exec() runs the pipeline and returns the results. ioredis pipelines are non-transactional by default; use .multi() instead of .pipeline() for a MULTI/EXEC block.