Modelled on src/networking.c and src/server.c, Redis 7.2.14. Defaults read from that release's src/config.c; buffer constants from src/server.h.
A client costs Redis three buffers: one sds for what it sent, one flat
reply buffer, and a list of heap blocks for whatever did not fit. Only the third is
policed by client-output-buffer-limit, and for a normal client that
limit is 0 0 0 — off (config.c:170). The page most people remember
says 256mb/64mb/60; that is the replica preset. Pick a class, run a scenario, and
watch which checks actually fire.
The last control is this page's own knob, not a Redis constant: a short
connWritev is what leaves sentlen pointing into a
half-sent block (networking.c:1864, 1880–1900).
One sds per client. Each readable event copies at most
PROTO_IOBUF_LEN = 16,384 bytes into it (networking.c:2653), then
processInputBuffer parses and trims with sdsrange
(networking.c:2626–2629).
qbuf in CLIENT LIST.0Growth: sdsMakeRoomFor, greedy — doubles below SDS_MAX_PREALLOC (sds.c:263, sds.h:36).
| limit | default | on breach |
|---|---|---|
| client-query-buffer-limit | 1 GB | WARNING log Closing client that reached max query buffer length, then freeClientAsync. No error reply. Checked after every read; master clients exempt. networking.c:2725–2734 |
| proto-max-bulk-len | 512 MB | -ERR Protocol error: invalid bulk length to the client, then CLIENT_CLOSE_AFTER_REPLY. networking.c:2356–2360, 2264 |
| PROTO_IOBUF_LEN | 16 KB | Not a limit — the per-event read size. server.h:176 |
| pre-AUTH bulk length | 16 KB | On a password-protected server, before AUTH: -ERR Protocol error: unauthenticated bulk length. networking.c:2361–2364 |
Not char[16384]. Since 7.0 it is a heap pointer sized by
buf_usable_size, allocated once in createClient
(networking.c:133) and resized per client between 1 KB and 16 KB by
clientsCronResizeOutputBuffer (server.c:771–812).
buf_usable_size - bufpos and returns a short count, so one reply can straddle buffer and list (networking.c:337–339).0Counted by the output buffer limit: no — getClientOutputBufferMemoryUsage ignores bufpos (networking.c:3799–3801).
Sticky: once the list is non-empty, _addReplyToBuffer returns 0
immediately and every later reply goes here, to keep order (networking.c:335).
A new block is only allocated after the tail's free space is used up
(networking.c:357–366).
size, i.e. allocated bytes, not used bytes (networking.c:378).0| class | hard | soft | seconds |
|---|---|---|---|
| normal | 0 | 0 | 0 |
| pubsub | 32mb | 8mb | 60 |
| replica | 256mb | 64mb | 60 |
Zero means unconfigured: networking.c:3900–3905 only sets the hard or
soft flag when the configured byte count is non-zero, so a stock normal client
has no output buffer limit at all. A replica's usage is not c->reply at all
— it reads the shared replication buffer through ref_repl_buf_node
(networking.c:3788–3798), so it is out of scope here.
Before AUTH on a password-protected server the class is bypassed: over 1 KB of output is an immediate close (networking.c:3882–3883).
maxmemory-clients (default 0, disabled —
config.c:3248) is the other cap: a server-wide ceiling on the combined memory of all
normal and pubsub clients, which evicts the largest clients first
(networking.c:4589–4590, 4583). It is the only limit that counts querybuf, reply
buffer and reply list in one number (networking.c:3808–3814).
Two steps, not one. closeClientOnOutputBufferLimitReached only
detects; it never closes the socket itself.
detect _addReplyProtoToList allocates a new block networking.c:380
closeClientOnOutputBufferLimitReached(c, 1)
reply_bytes == 0 ? return 0 networking.c:3945
checkClientOutputBufferLimits(c) networking.c:3947
freeClientAsync(c) networking.c:3951
c->flags |= CLIENT_CLOSE_ASAP networking.c:1736
listAddNodeTail(server.clients_to_close, c) networking.c:1742
log "Client %s scheduled to be closed ASAP for
overcoming of output buffer limits." networking.c:3953
reap beforeSleep server.c:1745
freeClientsInAsyncFreeQueue() networking.c:1797
The check runs where a block is allocated — networking.c:380, 833 and
1217 — so an append that fits in the tail triggers no check at all. Separately,
clientsCron calls it with async = 0 (server.c:1042), which takes
the freeClient branch and closes the client on the spot
(networking.c:3956). The same cron pass also resizes the query buffer (server.c:1028) and
the reply buffer — different work, same tick.