Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Redis with Python: Connect Once, Decode Responses, and Mind the Bytes

Sean

Platform Writer

Jul 18, 2026
7 min read

To use Redis from Python, install redis (the redis-py client) and connect: r = redis.Redis(host='localhost', port=6379, decode_responses=True). Then r.set('key', 'value') and r.get('key') do what you expect. The two things beginners miss: set decode_responses=True or every response comes back as raw bytes, and reuse one client across your app rather than connecting per request - redis-py pools connections for you. Get those right and Redis from Python is genuinely simple.

Redis with Python: Connect Once, Decode Responses, and Mind the Bytes

Table of contents

Install and connect

pip install redis
import redis

r = redis.Redis(
    host="localhost",
    port=6379,
    db=0,
    decode_responses=True,     # get str back, not bytes
)

r.set("greeting", "hello")
r.get("greeting")              # 'hello'

redis.Redis(...) creates a client. Behind the scenes it manages a connection pool, so you create this object once - at application startup - and reuse it everywhere. You do not open a new connection per operation; the pool handles that.

The decode_responses=True argument is the one to remember, covered next. Without it, r.get("greeting") returns b'hello' - bytes, not a string - which trips up every newcomer. For a modern app working with text, set it and move on.

The bytes-versus-strings gotcha

By default, redis-py returns bytes, because Redis stores everything as raw bytes and the client does not assume an encoding:

r = redis.Redis()             # no decode_responses
r.set("name", "Sam")
r.get("name")                 # b'Sam'  - bytes, note the b prefix

That b'Sam' is a bytes object, and it will not compare equal to the string "Sam", which causes confusing bugs - r.get("name") == "Sam" is False. The fix is to decode responses at the client level:

r = redis.Redis(decode_responses=True)
r.get("name")                 # 'Sam'  - a proper string

With decode_responses=True, the client decodes every response from UTF-8 bytes to str for you. For text-based applications this is what you want, and setting it once on the client is far cleaner than calling .decode() on every value. The only time to leave it off is when you are genuinely storing binary data - images, serialized blobs - where bytes are correct.

The core data-type operations

Redis is more than a string store; redis-py maps each Redis type to Python methods:

# strings and counters
r.set("hits", 0)
r.incr("hits")                # atomic increment -> 1

# hashes (like a dict)
r.hset("user:1", mapping={"name": "Sam", "age": "30"})
r.hgetall("user:1")           # {'name': 'Sam', 'age': '30'}

# lists (like a queue)
r.rpush("jobs", "a", "b")
r.lpop("jobs")                # 'a'

# sets
r.sadd("tags", "python", "redis")
r.smembers("tags")            # {'python', 'redis'}

# expiry
r.set("token", "xyz", ex=60)  # expires in 60 seconds

The method names mirror the Redis commands - SET is .set, INCR is .incr, HGETALL is .hgetall. If you know the Redis command, you know the method. incr is worth calling out: it is atomic, so it is the correct way to count things across processes without a race - two workers incrementing at once both get counted, which a read-modify-write in Python would not guarantee.

Pipelines cut round trips

Each Redis command is a network round trip. When you issue many commands, those round trips add up. A pipeline batches them into one:

pipe = r.pipeline()
pipe.set("a", 1)
pipe.set("b", 2)
pipe.incr("counter")
results = pipe.execute()      # one round trip for all three

Instead of three separate trips to the server, the pipeline sends all the commands together and gets all the results back at once. For bulk operations - loading many keys, updating a batch - this is a large speedup, because network latency, not Redis itself, is usually the bottleneck.

Pipelines can also be transactional. Wrap them so the commands execute atomically:

with r.pipeline() as pipe:
    pipe.multi()
    pipe.incr("balance")
    pipe.execute()

Reach for a pipeline whenever you are about to issue several commands in a row with no dependency between them. It is the single easiest Redis performance win in application code, and it costs almost nothing to adopt.

Connection reuse and errors

Two operational points that separate a toy from a service.

First, reuse the client. Creating a redis.Redis(...) per request exhausts connections and adds latency. Create one at startup and share it - the pool inside it is thread-safe and hands out connections as needed:

# module-level, created once
r = redis.Redis(host="localhost", decode_responses=True)

Second, handle the failure that will happen: Redis being unreachable. Wrap operations where a Redis outage should not take down your app:

try:
    value = r.get("key")
except redis.ConnectionError:
    value = None              # fall back gracefully

Redis is often a cache, and a cache being down should degrade to a slower path, not a crash. Catching redis.ConnectionError and falling back to the source of truth is what makes Redis an optimization rather than a single point of failure. Design for it being occasionally unavailable, because over a long enough time, it will be.

How this fits the rest of the stack

A Redis client that connects once, decodes cleanly, and degrades gracefully when the cache is down is the difference between Redis speeding your app up and Redis being the thing that takes it offline. When both the app and its Redis run as managed services, the connection details and the failure handling are exactly what you want to get right before real traffic arrives. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.

Useful related references:

FAQ

How do I connect to Redis from Python?

Install the client with pip install redis, then create r = redis.Redis(host='localhost', port=6379, decode_responses=True). Use r.set('key', 'value') and r.get('key') to read and write. Create the client once at startup and reuse it - redis-py pools connections internally.

Why does redis-py return bytes instead of strings?

Because Redis stores raw bytes and the client does not assume an encoding by default, so r.get('name') returns b'Sam'. Pass decode_responses=True when creating the client and responses come back as proper strings. Leave it off only when storing genuine binary data.

How do I make Redis operations faster in Python?

Use a pipeline to batch commands into a single network round trip: create pipe = r.pipeline(), queue several commands, then call pipe.execute(). Since network latency is usually the bottleneck, pipelining bulk operations is the easiest large performance win in application code.

Should I create a new Redis connection per request?

No. Create one redis.Redis(...) client at application startup and share it. The client manages a thread-safe connection pool that hands out connections as needed. Creating a client per request exhausts connections and adds unnecessary latency.

How do I handle Redis being unavailable in Python?

Wrap operations in a try/except for redis.ConnectionError and fall back gracefully - return None or read from the source of truth. Since Redis is often a cache, an outage should degrade to a slower path rather than crash the app.

#redis python#redis#python#redis-py#dev-infra