Skip to content

Redis - Pipelining

Redis operates on a request/response protocol over a TCP connection. Typically, a client sends a command and waits for the server’s response before sending the next command. This round-trip time (RTT) for each command can introduce significant latency, especially over a network.

Pipelining is a client-side optimization that dramatically improves performance. Instead of sending one command and waiting for the response, a client can send multiple commands to the server at once without waiting. The server processes these commands in order and sends all the responses back to the client in a single batch. This minimizes the impact of network latency by reducing the number of round trips.

You can observe the raw Redis Serialization Protocol (RESP) in action using a command-line tool like netcat (nc). This demonstrates how multiple commands can be buffered and sent in a single write operation.

# The following command sends five commands to Redis in a single payload.
# The server will queue them and send all replies back at once.
(printf "PING\r\nSET user:1:session 'active'\r\nGET user:1:session\r\nINCR page:views\r\nGET page:views\r\n"; sleep 2) | nc localhost 6379
# Expected Output:
+PONG
+OK
$6
active
:1
:1

In this example, we sent PING, SET, GET, INCR, and another GET command together. The server responded with all five replies sequentially, significantly reducing the total time compared to five individual request/response cycles.

Practical Application: Pipelining with a Client Library

Section titled “Practical Application: Pipelining with a Client Library”

In a real-world application, you won’t use netcat. Modern Redis client libraries provide a straightforward API for pipelining. Here’s an example using Python’s popular redis-py library.

# First, ensure you have the library installed:
# pip install redis
import redis
# Connect to your Redis instance
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Create a pipeline object
pipe = r.pipeline()
# Queue commands. These are NOT sent to the server yet.
pipe.set('user:profile:100', '{"name": "Alice", "email": "alice@example.com"}')
pipe.incr('login:attempts:100')
pipe.expire('user:profile:100', 3600) # Expire the profile in 1 hour
pipe.get('user:profile:100')
# Execute all queued commands in a single round-trip
# The results are returned as a list in the same order.
try:
results = pipe.execute()
print("Pipeline executed successfully.")
print("Results:", results)
# Expected output:
# Pipeline executed successfully.
# Results: [True, 1, True, '{"name": "Alice", "email": "alice@example.com"}']
except redis.RedisError as e:
print(f"An error occurred: {e}")
  • Performance: Drastically reduces latency by minimizing network round-trip time. The performance gain can be 5-10x on a local network and over 100x on high-latency connections.
  • Efficiency: Reduces CPU usage on both the client and server as fewer I/O system calls are needed.
  • Memory Usage: The client must buffer all commands and the server must buffer all replies. Pipelining a very large number of commands can lead to increased memory consumption.
  • Not Atomic: Pipelining is an optimization for network traffic; it is not the same as a transaction. While commands are executed in order, another client’s command could be interleaved between the execution of your pipelined commands. For atomicity, use Redis Transactions (MULTI/EXEC).
  • Use for Bulk Operations: Pipelining is ideal when you need to run a large number of commands without needing the result of one command to determine the next one.
  • Batch Sensibly: Don’t pipeline millions of commands at once. Batch them into reasonable chunks (e.g., 1,000 to 10,000 commands per pipeline) to balance performance and memory usage.
  • Combine with Transactions: You can pipeline a MULTI/EXEC block to get both atomicity and network performance.