What Is Redis Pipelining?

Pipelining is the practice of sending several Valkey or Redis commands to the server without waiting for each reply before sending the next one. The client writes the whole group to the connection at once, the server executes the commands in order, and the replies come back together. What would have been N network round trips becomes one.

The technique is also called batching, and the two words are used interchangeably. In Redisson, the Valkey and Redis Java client, pipelining is exposed through the RBatch object.

Pipelining matters because of a simple piece of arithmetic. Most Valkey and Redis commands execute in microseconds — the server is rarely the bottleneck. The waiting is. On a link with a half-millisecond round trip, a loop issuing a thousand sequential GET commands spends roughly half a second on the network and a few milliseconds doing actual work. Pipelined, the same thousand commands cost one round trip plus that same few milliseconds of execution. The server did not get faster; you simply stopped paying the network a thousand times.

How Pipelining Works

In the usual request/response cycle, a client writes one command and blocks until the reply arrives. The RESP protocol that Valkey and Redis speak does not actually require this. Nothing stops a client from writing a second command before the first reply has come back.

Pipelining exploits exactly that. Commands are written to the socket back to back, the server reads them off as fast as they arrive, executes each in sequence, and writes the replies out. Because the server never reorders commands on a single connection, the nth reply always corresponds to the nth command — which is what allows a client to match results to commands by position rather than by any kind of identifier.

There is a cost, and it is memory. While the batch runs, the server queues up replies that the client has not yet read, and that queue is real memory on the server. A pipeline of ten million commands is a very effective way to inflate it. The remedy is to chunk: the Valkey and Redis documentation suggests batches on the order of 10,000 commands — send a batch, read the replies, then send the next. Throughput is essentially unchanged, because the round-trip saving has already saturated well before that size, while the memory held at any one moment stays bounded.

Pipelining also reduces work on the server itself. Reading ten commands from one socket read costs far less than ten separate reads, so a heavily pipelined workload consumes noticeably less CPU in system calls than the same commands sent one at a time. The latency win is the headline, but the throughput win is real too.

Redis Pipelining vs Transactions (MULTI/EXEC)

This is the distinction that causes the most confusion, and it comes down to one word: interleaving.

A pipeline guarantees that your commands run in the order you sent them. It does not guarantee that nothing else runs in between. Another client's commands can be executed between the third and fourth commands of your batch. A transaction opened with MULTI and closed with EXEC does prevent that — the queued commands run as one isolated step with nothing interleaved.

The mistake teams make is choosing a transaction for performance. MULTI/EXEC is itself normally pipelined — the whole block travels to the server in a single round trip — so a transaction is not faster than a pipeline. It is simply a pipeline with an isolation guarantee bolted on, and isolation is not free. If what you want is throughput, pipelining already gives you the entire network saving with none of the added cost.

PipeliningTransaction (MULTI/EXEC)
Network round tripsOne for the whole batchOne, when the block is pipelined
Execution orderPreservedPreserved
Other clients can interleaveYesNo
AtomicNoAt queue time only
Rollback after a failureNoneNone
If one command failsThe rest still executeThe rest still execute
Best forThroughput on independent commandsIsolating a group of dependent writes

Look closely at the two failure rows, where the columns agree. Neither mechanism rolls back. Once a native transaction begins executing, a command that fails at runtime does not undo the ones before it, and DISCARD only helps before EXEC. If you need genuine rollback across several objects, that is a different tool again — Redisson's RTransaction, covered in How to Manage Transactions in Valkey and Redis on Java.

The practical rule: reach for pipelining when the commands are independent and you want them to go fast, and for a transaction when they must not be observed half-applied.

When Pipelining Helps — and When It Doesn't

Pipelining pays off whenever a loop issues one command per iteration: bulk loading a dataset, warming a cache after a deploy, fanning out reads across many keys, or writing a burst of records where you do not need each reply before continuing.

It does not help in three situations, and recognising them saves a lot of wasted effort.

When one command depends on the result of another. A pipeline sends everything before reading anything, so it cannot branch on a reply. If you need to read a value, compute something from it, and write the result, the read has to complete first — no amount of batching changes that. Moving the logic to the server with a Lua script is the usual answer: one round trip, and the read-compute-write runs as one atomic step. Pipelining and scripting solve adjacent problems, and it is worth knowing which one you actually have.

When a native multi-key command already exists. A pipeline of a hundred GET commands is slower and more verbose than a single MGET, which is one command with one reply. Reach for MGET, MSET, or HMGET first; pipelining is what you use when the commands are heterogeneous enough that no single command covers them.

When the workload is not network-bound. Pipelining removes waiting; it does not make commands cheaper. If your bottleneck is a handful of O(N) commands over large collections, or memory pressure, batching them changes very little.

There is also a trade-off worth naming explicitly. The latency of any individual command inside a batch goes up, because its reply is not available until the batch completes. Pipelining buys throughput by spending per-command latency. For bulk work that is an excellent trade. For a single request on a user's critical path, it is not a trade at all.

Pipelining Across a Cluster

In a cluster, keys are distributed across hash slots owned by different nodes, and a single connection talks to a single node. A batch touching keys in several slots therefore cannot be one request — it has to be split by owning node, dispatched separately, and reassembled in the original order.

Low-level clients push that work onto you. The alternative, forcing keys into one slot with a {} hash tag, works but defeats the point of sharding for bulk operations: everything lands on one node.

This is where a higher-level client earns its place. Redisson executes a batch in a cluster in map/reduce fashion, grouping commands by node, sending the groups simultaneously, and merging the per-node results back into a single ordered response list. The code you write is identical to the single-node case.

Pipelining in Java With Redisson

Create a batch, queue commands by calling the asynchronous methods on its objects, and execute:

RBatch batch = redisson.createBatch(BatchOptions.defaults());

RMapAsync<Long, Product> products = batch.getMap("products");
products.getAsync(101L);
products.getAsync(102L);
products.getAsync(103L);

// every queued call also hands back a future you can read on its own
RFuture<Long> views = batch.getAtomicLong("views:101").incrementAndGetAsync();

// one network round trip for everything queued above
BatchResult<?> result = batch.execute();

List<?> responses = result.getResponses();   // in the order the commands were queued
Product first = (Product) responses.get(0);
Long viewCount = views.toCompletableFuture().join();

Nothing is sent until execute() is called. Each result can be read either from the RFuture that the queued method returned, or positionally from getResponses().

BatchOptions controls how the batch behaves:

BatchOptions options = BatchOptions.defaults()
    // IN_MEMORY (default) buffers commands client-side, then sends them.
    // IN_MEMORY_ATOMIC, REDIS_READ_ATOMIC and REDIS_WRITE_ATOMIC
    // execute the batch atomically instead.
    .executionMode(BatchOptions.ExecutionMode.IN_MEMORY)

    // discard replies entirely — worthwhile for large write-only batches
    .skipResult()

    // wait for writes to reach 2 replicas, up to 1 second
    .syncSlaves(2, 1, TimeUnit.SECONDS)

    .responseTimeout(2, TimeUnit.SECONDS)
    .retryInterval(2, TimeUnit.SECONDS)
    .retryAttempts(4);

Two of these deserve attention. skipResult() tells the server not to send replies at all, which removes the reply-queue memory pressure described earlier — ideal for bulk loads where you only care that the writes landed. Leave it off whenever you actually need results, since there will be none to read. And the execution modes are how a Redisson batch becomes atomic when you need it to: IN_MEMORY is a plain pipeline, while the *_ATOMIC variants run the queued commands as a single atomic unit.

That separation is deliberate. Some clients overload one API for both jobs — Valkey GLIDE's Batch(isAtomic) flag is the clearest example — which makes it easy to pay for atomicity you never wanted. Redisson keeps RBatch for throughput and RTransaction for transactional semantics with real rollback, so the choice is explicit.

Batches are available across every Redisson programming model — RBatch covers the synchronous and asynchronous styles, with RBatchReactive and RBatchRx for Project Reactor and RxJava3. The full reference, including per-model examples, is in the pipelining documentation.

One last thing to keep an eye on: every batched command still pays serialization cost for its arguments and results. Once the network round trips are gone, codec choice is often the next thing that shows up in a profile.

Redis Pipelining: Frequently Asked Questions

What Is the Difference Between Redis Pipelining and a Transaction?

Pipelining sends multiple commands in one round trip to save network time; it guarantees the order of your commands but allows other clients' commands to interleave with them. A MULTI/EXEC transaction prevents interleaving by running the group as one isolated step. Because a transaction block is normally pipelined too, transactions are not faster — they add isolation, not speed.

Is Redis Pipelining Atomic?

No. The commands in a pipeline execute in the order sent, but other clients can run commands in between them, and a command that fails does not stop or undo the rest. If you need atomicity, use a transaction; if you need atomicity with rollback, use Redisson's RTransaction or a server-side Lua script.

How Many Commands Should You Put in One Pipeline?

The official guidance is around 10,000 commands per batch. The server queues every reply in memory until the client reads them, so a much larger pipeline inflates server memory without buying speed — throughput has already saturated well before that point. Send 10k, read the replies, send the next 10k. For write-heavy bulk loads, Redisson's skipResult() removes the replies from the equation entirely.

Does Pipelining Work With Redis Cluster?

Yes, but the batch has to be split across the nodes owning the relevant hash slots and the results merged back together. Redisson does this automatically, executing the batch in map/reduce fashion. With a low-level client you either partition the batch yourself or collocate keys with a {} hash tag, which concentrates the work on a single node.

Does Pipelining Reduce Server Load, or Only Latency?

Both. The headline benefit is eliminating round trips, but reading many commands from a single socket read also cuts the number of system calls the server performs, so a pipelined workload consumes measurably less server CPU than the same commands sent individually.

Similar terms