skip to content

While one Redis client is sending a batch of commands in a single pipeline, can another client's commands execute in between them? State precisely what guarantees pipelining does and does not give.

level: middleimportance: must knowfreq 52%

answer

  1. transport batching, not execution batching
  2. other clients interleave between your commands
  3. order per connection is the only ordering promise
  4. crash leaves an arbitrary prefix applied
  5. need indivisibility, use MULTI/EXEC or Lua

basics

~20 s

Yes, other clients can run in between. A pipeline only guarantees that your commands execute in the order you sent them on that connection, and that replies come back in that order. It gives no atomicity, no isolation, and no all-or-nothing behaviour on failure.

solid answer

~50 s

Pipelining is a transport optimisation, not a transaction. Redis parses whatever complete commands are available in each client's query buffer and executes them one at a time; between two commands of my pipeline it can freely execute commands from any other client. So a pipelined `GET balance` then `SET balance <new>` is just as racy as two separate round trips. What it does guarantee: **per-connection ordering** (my commands run in the order I wrote them) and **reply ordering** (reply *i* answers command *i*). Nothing more. What it does not guarantee: atomicity, isolation from other clients, or all-or-nothing execution. If the connection drops or the server restarts halfway through, an arbitrary prefix may already be applied, and I may never learn which. Errors do not stop the batch either; command 3 failing does not prevent commands 4 to 100 from running. If I need indivisibility I wrap the block in `MULTI`/`EXEC`, or use a Lua script when the batch needs read-then-decide logic.

code

text · 11 lines
text
# RACY: both commands are pipelined, but another client can INCR between them
GET counter        -> "41"
SET counter 42     # lost update if someone else incremented meanwhile

# INDIVISIBLE: whole block runs with no other client's command inside it,
# and it is still sent as ONE pipelined write, so ONE round trip
MULTI
INCR counter
EXPIRE counter 3600
EXEC               -> 1) (integer) 42
                      2) (integer) 1

go deeper

for a junior

Answer the yes/no directly: yes, other clients can run in between, because a pipeline only batches network traffic.

for a middle

Name the two real guarantees (per-connection command order, matching reply order) and the three non-guarantees (atomicity, isolation, all-or-nothing on failure).

for a senior

Discuss the consequences you have actually handled: idempotent batch design, inspecting every reply because errors do not short-circuit, and picking MULTI/EXEC or Lua as the correct atomicity unit.

for a principal

Frame it as choosing the system's unit of atomicity: which invariants must hold under interleaving, where that pushes logic server-side, and the operational cost of scripts holding the single execution loop versus racy client-side composition.

## The mental model: one shared execution queue Redis processes client work on a single command-execution loop. Each connected client has its own input (query) buffer and output buffer. The server repeatedly picks up readable connections, parses **complete** commands out of each query buffer, executes them, and appends replies to that client's output buffer. Nothing in this design associates a group of commands with a client 'turn'. Pipelining changes only how the bytes arrived; the scheduler still moves between clients at command granularity. The consequence is direct: if client A pipelines `[C1, C2, C3]` and client B sends `X`, a legal execution order is `C1, X, C2, C3`. There is no fencing, no per-key lock, no batch boundary the server is aware of. From the server's perspective a pipeline is indistinguishable from the same commands arriving separately but quickly. ## The guarantees you do get 1. **Ordering within the connection.** Commands from one connection execute in the byte order they were written. Redis will never run C2 before C1. 2. **Reply ordering.** Replies are appended to the output buffer in execution order, so the *n*th reply answers the *n*th command. This is what lets clients pop a pending-command queue positionally instead of correlating IDs. 3. **Per-command atomicity.** Each individual command is still indivisible, as always in Redis. `INCR` inside a pipeline is exactly as atomic as `INCR` alone. The atomicity boundary is the command, not the batch. ## The guarantees you do not get - **No isolation.** Read-then-write logic split across two pipelined commands is a classic race: two clients both pipeline `GET counter` and `SET counter <value+1>` and one increment is lost. Pipelining actually widens nothing, but it also fixes nothing; people wrongly assume the batch is protected. - **No all-or-nothing on failure.** If the socket breaks, the process is killed, or the server fails over mid-batch, a prefix of the commands has run. Worse, the client may not know how long that prefix is, because the replies it never read may still have been produced. Pipelined batches should therefore be composed of idempotent commands (`SET` of an absolute value rather than `INCRBY`, where possible), or be replayable. - **No error short-circuit.** A command that errors returns an error reply and the batch continues. If command 3 returns `WRONGTYPE`, commands 4 onward still execute. Clients that ignore replies silently swallow such errors, which is one of the most common production bugs around pipelining. - **No cross-client ordering.** Two clients pipelining concurrently interleave arbitrarily. Do not use pipeline boundaries to reason about relative ordering between clients. ## Getting indivisibility when you need it - **`MULTI` ... `EXEC`.** Commands issued after `MULTI` are queued server-side and executed as one uninterrupted unit at `EXEC`; no other client's command runs inside that unit. The transaction and the pipeline are orthogonal and are usually combined: send `MULTI`, the commands, and `EXEC` in one pipelined write so you also save the round trips. - **`WATCH` plus `MULTI`/`EXEC`** for check-then-act on a key, which is the optimistic-concurrency form: `EXEC` returns a null reply if a watched key changed. - **Lua scripts / Functions** when the batch needs to *branch* on a value it read, since a transaction cannot look at intermediate results. ## Diagnosing the misconception in practice Symptoms of assuming pipeline atomicity are lost updates under load, counters that drift, and 'impossible' interleavings in logs. A quick test: run the suspect batch from two clients in a tight loop and assert the invariant; it will break within seconds. The fix is almost never a bigger pipeline, it is choosing the right unit of atomicity, which is a single command, a `MULTI`/`EXEC` block, or a script. ## What an interviewer is listening for A crisp separation of two ideas: **batching bytes** (pipelining) versus **batching execution** (transactions/scripts). Candidates who conflate them tend to ship read-modify-write races. Bonus points for mentioning partial application on connection loss and that errors do not abort the rest of the batch.

  • If pipelining is not atomic, why do client libraries still send MULTI/EXEC inside a pipeline?
    Because the two solve different problems and compose. MULTI/EXEC supplies indivisible execution, while pipelining removes the round trip that each queued command would otherwise cost, since every queued command normally gets its own +QUEUED reply. Sending MULTI, the body, and EXEC in one write gives one round trip for the whole transaction plus atomic execution.
  • Your pipelined batch of 500 commands returned successfully but one key looks wrong. How do you find out what happened?
    Inspect every reply, not just the last one: Redis returns an error reply in place for a failed command and keeps executing the rest, so a client that discards replies hides real failures. Check for error replies at the matching position, and confirm the command was appropriate for the key's type. If the connection dropped instead, assume an arbitrary prefix was applied and re-run an idempotent version of the batch.

saying these in an interview costs you the question

  • 'A pipeline is basically a transaction'
  • 'Other clients are blocked until my pipeline finishes'
  • Assuming a failing command aborts the remaining pipelined commands
  • Assuming a dropped connection means nothing was applied
  • Using pipelined GET then SET as a safe read-modify-write

context