How do you choose the batch size when pipelining commands to Redis, and which client-side and server-side limits does an oversized batch run into?
answer
- RTT win saturates by a few hundred commands
- query buffer: client-query-buffer-limit 1 GB
- normal client output buffer: 0 0 0 = unbounded
- read replies as you flush, keep buffers bounded
- one giant batch starves every other client
basics
~20 sSize batches by bytes, not just count: a few hundred to a few thousand commands, roughly under a few megabytes of requests plus replies. Too large hits the server's per-client query-buffer limit, inflates the client output buffer (unlimited for normal clients by default), and monopolises the command loop.
solid answer
~50 sI pick batch size empirically against a byte budget rather than a magic command count. Most of the RTT win arrives by a few hundred commands per flush, so 100-1,000 is a common sweet spot; large values (multi-KB payloads) push me to smaller batches. The limits are on both sides. Server side, unread inbound bytes accumulate in that client's **query buffer**, capped by `client-query-buffer-limit` (1 GB default) and single argument size by `proto-max-bulk-len` (512 MB); exceeding them kills the connection. Replies accumulate in the client's **output buffer**, and for normal clients `client-output-buffer-limit normal 0 0 0` means *unbounded*, so a client that streams a million commands without reading replies can drive the server into memory pressure or OOM. There is also a fairness cost: one giant batch keeps the single command loop busy, adding latency for every other client. Practical rule: flush every N commands **and** read the replies as you go, so nothing accumulates unboundedly. `redis-cli --pipe` does exactly that.
code
text · 12 lines# server-side caps that a huge pipeline can hit
CONFIG GET client-query-buffer-limit # 1gb - inbound, per client
CONFIG GET proto-max-bulk-len # 512mb - single argument cap
CONFIG GET client-output-buffer-limit
# 1) "normal" 2) "0 0 0" <- NO limit for ordinary clients
# 3) "slave" 4) "268435456 67108864 60"
# 5) "pubsub" 6) "33554432 8388608 60"
# watch a client that pipelines without reading replies
CLIENT LIST
# id=42 addr=10.0.0.7:5310 ... qbuf=1048576 qbuf-free=20 omem=734003200 cmd=get
# ^ inbound backlog ^ reply memory the SERVER holdsgo deeper
Know that batches should be moderate (hundreds to a few thousand) and that you should read replies rather than accumulating them all.
Name the two buffers, note that the RTT benefit saturates, and mention proto-max-bulk-len and client-query-buffer-limit as hard caps.
Show the operational angle: unbounded normal-client output buffers as a memory incident, CLIENT LIST omem/qbuf as evidence, fairness impact on p99, and streaming flush-and-read as the fix.
Treat depth as a tuned system parameter: measured throughput/latency curve, memory attribution and blast radius per client, guardrails in the shared client library, and how bursty batches interact with replication and persistence load.
## Why there is a ceiling at all Pipelining wins by removing round trips, and that win saturates. Going from depth 1 to 16 typically multiplies throughput several-fold; going from 1,000 to 100,000 buys almost nothing, because at that point the limits are CPU, bandwidth, and memory rather than RTT. Meanwhile the costs of depth grow linearly. So the sizing question is really: what is the smallest batch that captures the RTT saving, and what breaks if I go far beyond it? ## Server-side limits **Query buffer.** Each client connection has an input buffer where bytes accumulate until complete commands can be parsed. A pipelined write lands there in bulk. `client-query-buffer-limit` (default 1 GB) caps it; a client that exceeds it is closed with an error, and you will see it in the log and in `CLIENT LIST` as a growing `qbuf`. A single argument is separately capped by `proto-max-bulk-len` (default 512 MB), which matters when the batch contains large values rather than many small commands. **Output buffer.** Replies are appended to a per-client output buffer and drained as the socket becomes writable. This is the dangerous one: `client-output-buffer-limit normal 0 0 0` means normal (non-replica, non-pubsub) clients have **no** hard limit, soft limit, or soft seconds. If the client keeps writing commands and never reads, the server keeps buffering replies in RAM. A million `GET`s of 1 KB values is a gigabyte of reply buffer attributed to one client, which can trip `maxmemory` behaviour, trigger evictions, or push the process into the OOM killer. `CLIENT LIST` shows this as rising `omem`, and `INFO clients` reports `client_recent_max_output_buffer`. **Fairness and latency.** Redis executes commands one at a time on a single loop. A huge readable batch means a long uninterrupted stretch of work for one client, during which every other client waits. This inflates p99 latency across the whole instance and can show up as unrelated timeouts. Splitting the same work into moderate batches interleaves naturally. ## Client-side limits The client must retain one entry per in-flight command so it can match replies positionally, so deep pipelines cost client memory too, and many drivers buffer the entire outgoing batch before writing. Deferring all reply parsing to the end also means an error at command 7 is not noticed until command 1,000,000 has already been sent. Some drivers additionally have a per-request write timeout that a huge single write can exceed. A related trap is **flow-control deadlock reasoning**: people assume that if the client stops reading, TCP back-pressure will simply pause the server. It does not translate into safety, because the server absorbs the back-pressure as heap memory in the output buffer rather than blocking, which is precisely why unbounded batches turn into memory incidents. ## A practical sizing method 1. Estimate bytes, not commands: `batch_bytes = N x (avg request size + avg reply size)`. Target something on the order of hundreds of KB to a few MB per flush. 2. Start at 100-1,000 commands per flush, measure end-to-end throughput and the instance's p99 latency, and stop increasing when throughput flattens or other clients' latency degrades. 3. **Read replies as you flush.** Streaming (write batch, read batch, repeat) keeps both buffers bounded and surfaces errors early. 4. For bulk import, use `redis-cli --pipe`, which streams raw RESP and reconciles reply counts and errors at the end. 5. Watch `CLIENT LIST` (`qbuf`, `qbuf-free`, `omem`, `age`), `INFO clients`, and the slow log while tuning. 6. Prefer variadic commands (`MSET`, `MGET`, multi-member `SADD`) where they apply: same round-trip saving, fewer commands to buffer, and one execution unit. ## Sharded and replicated deployments When many keys are involved you cannot always keep one flat batch, because commands must be routed per destination; drivers typically split the batch per connection and re-merge the replies. That splitting also naturally bounds per-connection batch size. Independently, all those writes still propagate to replicas and the AOF, so a huge burst can spike replication backlog use and disk write pressure, which is another reason to smooth rather than spike. ## What an interviewer is listening for That you size by bytes and measured effect rather than folklore, that you know the reply side is the memory risk because normal clients are unlimited by default, and that you connect oversized batches to *other* clients' latency, not just your own throughput.
- A client pipelines a million commands and never reads the replies. What fails first, and what would you see?The server keeps appending replies to that client's output buffer, and because the default limit for normal clients is 0 0 0 the buffer is unbounded, so used_memory climbs and is attributed to that connection. You would see omem growing in CLIENT LIST and client_recent_max_output_buffer in INFO clients, then eviction or an OOM condition depending on maxmemory. The fix is streaming: flush in bounded batches and read replies as you go.
- Would you rather pipeline 1,000 SET commands or send one MSET with 1,000 pairs?MSET is usually better: one command means one round trip, less parsing overhead, less per-command buffering, and one indivisible execution unit. The caveats are that MSET cannot express per-key TTLs or conditional writes, and in a sharded deployment all keys must live on the same shard, so a pipeline of individual SETs remains the general tool when keys or options vary.
saying these in an interview costs you the question
- 'Bigger pipelines are always faster'
- 'The server just blocks on TCP if I do not read replies' (it buffers in RAM instead)
- Assuming normal clients have an output-buffer limit by default
- Ignoring the effect of one giant batch on other clients' latency
- Sizing batches by command count alone with multi-KB values