Nowadays, almost all Redis deployments use TCP as the primary connection protocol. Redis also supports Unix Domain Socket (UDS) when the client and server run on the same machine. UDS bypasses the TCP/IP stack and network interface entirely, typically reducing latency by 30–40% compared to TCP localhost — though the exact gain depends on workload. Because UDS requires co-location, TCP remains the universal default.
Why Redis uses TCP?
TCP fits Redis for the following reasons
- Persistent, stateful connections: Redis processes one command at a time per client, in strict order: the client sends a command, waits for the reply, then sends the next. This request-response model requires a persistent, stateful connection, which TCP provides. Each TCP connection is uniquely identified by a 4-tuple (src_ip, src_port, dst_ip, dst_port), letting Redis maintain per-client state: current database index, transaction state (MULTI/EXEC), subscription lists, and so on. UDP is connectionless and stateless; a server would have to re-identify the client on every single datagram, pushing all that state management into application code.
- In-order delivery: Redis uses RESP (Redis Serialization Protocol), which is a stream-based protocol. Commands arrive as a continuous byte stream, and Redis parses them sequentially. If bytes arrived out of order, the parser would break. TCP guarantees the stream is always ordered and complete.
- Reliability: TCP automatically retransmits lost packets. Redis does not need to write any retry logic itself; the OS handles it transparently.
- Flow control: TCP's sliding window prevents a fast client from overwhelming Redis's read buffer. Every TCP packet Redis sends back to the client carries an rwnd (receive window) value, a number the OS stamps automatically, representing free space remaining in the buffer. The client treats this as a hard cap on how much data it can have in flight. As Redis reads and drains its buffer, rwnd grows, and the client can send more. If Redis falls behind, rwnd shrinks toward zero, and the client throttles itself automatically. Redis never writes a line of code for this — the kernel manages it entirely.
- Pipelining: Because TCP preserves byte order across the entire stream, clients can send many commands in one batch without waiting for individual replies. The server reads commands back-to-back from the stream and sends replies in the same order. This is pipelining, and it would be impossible without a reliable, ordered byte stream.

The above diagram describes three phases in Redis server when a Redis client connects for the first time
-
Accept phase
- The OS and Redis work asynchronously. When a client calls connect(), the OS kernel handles the entire TCP handshake (SYN → SYN-ACK → ACK) on its own, Redis is not involved. Once the handshake completes, the connection is pushed into the accept queue, sitting there until Redis is ready to pick it up.
- Meanwhile, Redis may be busy executing a command for another client. When it finishes, the event loop calls epoll_wait(). If a connection is waiting in the queue, epoll reports it, and only then does Redis call accept(). If Redis is already idle, it calls accept() almost immediately.
- Register phase: accept() returns a new file descriptor, an integer that uniquely identifies the connection (for example, fd = 7). Redis registers this fd with epoll (Linux) or kqueue (macOS/BSD). From this point, the OS automatically notifies Redis when data arrives on that fd, so Redis never has to poll in a loop. (To be more understandable, you should read about AE Event Loop).
- Allocate phase: Redis allocates a client struct in memory for the connection. This includes a 16 KB read buffer to hold incoming command bytes streaming in over TCP, and a write buffer to hold responses waiting to be sent back. The buffer exists because TCP can split a single command like SET key value across multiple small segments — Redis collects the bytes until it has a complete command before parsing.
The cost of a connection
Opening a connection requires:
- A file descriptor (an integer in the kernel)
- A client struct in Redis memory, including the 16 KB read buffer and write buffer — roughly ~20 KB of RAM per connection in total
- A slot in epoll's interest list
- epoll is a Linux kernel mechanism for monitoring multiple file descriptors simultaneously. Redis uses it to know when a client sends data — without constantly polling ("anything yet? anything yet?").
- epoll maintains an interest list inside the kernel — essentially a table of file descriptors that Redis has registered and wants to be notified about. Each entry in that table is a slot, containing: the file descriptor to watch (e.g. fd = 7), the event type to listen for (e.g. EPOLLIN — data is ready to read), and a pointer back to the corresponding client struct in Redis memory.
- When Redis calls epoll_ctl(ADD, fd) during the Register phase, it is essentially telling the kernel: "add this fd to the interest list, and notify me when it has data."
- Each slot occupies a small amount of kernel memory (a few dozen bytes). More importantly, epoll has a limit on how many fds it can watch simultaneously — a limit typically bounded by ulimit -n at the OS level. So every new connection doesn't just cost RAM on the Redis side; it also consumes a finite slot in the kernel's interest list. In systems with thousands of clients, microservices, workers, cron jobs, if every service opens its own dedicated connection, it is easy to hit Redis's maxclients limit (default: 10000) or the OS-level ulimit -n. The solution is connection pooling.
Connection Pooling
A connection pool is a group of TCP connections that are pre-created and reused. Instead of an application running connect() → use → close() on every request, the pool keeps connections alive and lends them out as needed. Two key configuration values to understand:
- minIdle — the minimum number of connections kept ready at all times. This reduces latency spikes when traffic ramps up suddenly, since connections are already warm.
- maxTotal — the upper limit on total connections in the pool. This prevents a single application from exhausting Redis's connection slots. Notice: connection pooling works best for long-running processes. In serverless or ephemeral environments where instances spin up and down frequently, persistent pooled connections can actually cause more churn, you may need a different strategy such as a sidecar proxy.
Pipelining
Normally, each command waits for the previous reply before the next command is sent. If the round-trip time (RTT) between client and server is 1 ms, 100 sequential commands take 100 ms of network wait time, even though each command executes in under 1 µs on the server.
Pipelining solves this by sending multiple commands in a single write, without waiting for each response. The client batches commands at the application layer; TCP delivers them in order; Redis reads and executes them sequentially and sends back all replies in one go. The result: 100 commands might complete in just over 1 ms instead of 100 ms.
One common misconception: pipelining is not a transaction. Commands are executed in order, but if command A fails, command B still executes. There is no atomicity. If you need all-or-nothing semantics, use MULTI/EXEC or a Lua script instead.
You can read the full details in the official Redis pipelining documentation.
Top comments (0)