StackPractices
beginner By Mathias Paulenko

Connect to Redis

How to connect to Redis in Python, JavaScript, and Java — with AUTH, TLS, pooling, and the core data types you'll actually use.

Topics: databases

Overview

Redis is an in-memory data store — reads and writes land in microseconds because nothing touches disk on the hot path. Connecting is easy; connecting right is where people get burned: no AUTH on an exposed port, no TLS over a public network, no pool, no maxmemory policy. Here’s the working setup in Python, JavaScript, and Java, plus the connection details most tutorials skip.

When to Use

Use this resource when:

  • Adding a cache layer in front of a database to cut latency and load — see cache-aside for the most common pattern
  • Storing sessions shared across app instances (stateless services can’t keep them in-process)
  • Building rate limiters, leaderboards, feature flags, or real-time counters
  • Passing lightweight messages between services with pub/sub or Streams

Redis is the wrong default when data must survive a restart by itself, when the dataset can’t fit in RAM, or when you need ad-hoc queries over relations — use PostgreSQL or MySQL for that, and keep Redis next to it, not instead of it.

Solution

Python

import os
import redis

# redis://default:password@host:6379/0 — use rediss:// for TLS
r = redis.Redis.from_url(
    os.getenv("REDIS_URL", "redis://localhost:6379/0"),
    decode_responses=True,
    socket_timeout=5,
    socket_connect_timeout=5,
    max_connections=20
)

# Strings
r.set('user:1:name', 'Alice', ex=3600)
name = r.get('user:1:name')

# Lists
r.lpush('queue:tasks', 'task_1')
task = r.brpop('queue:tasks', timeout=5)

# Hashes
r.hset('user:1', mapping={'email': 'alice@example.com', 'role': 'admin'})
user = r.hgetall('user:1')

JavaScript

const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
    // rediss:// URLs enable TLS automatically; add tls: {} to override certs
    maxRetriesPerRequest: 3,
    retryStrategy: (times) => Math.min(times * 200, 2000), // backoff, ms
    enableReadyCheck: true,
    lazyConnect: false
});

// Strings
await redis.set('user:1:name', 'Alice', 'EX', 3600);
const name = await redis.get('user:1:name');

// Lists
await redis.lpush('queue:tasks', 'task_1');
const task = await redis.brpop('queue:tasks', 5);

// Hashes
await redis.hset('user:1', 'email', 'alice@example.com', 'role', 'admin');
const user = await redis.hgetall('user:1');

Java

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.Map;

public class RedisExample {
    // For TLS use a URI: new JedisPool("rediss://default:pass@host:6379")
    private final JedisPool pool = new JedisPool("localhost", 6379);

    public void stringOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.setex("user:1:name", 3600, "Alice");
            String name = jedis.get("user:1:name");
        }
    }

    public void listOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.lpush("queue:tasks", "task_1");
            var task = jedis.brpop(5, "queue:tasks");
        }
    }

    public void hashOps() {
        try (Jedis jedis = pool.getResource()) {
            jedis.hset("user:1", "email", "alice@example.com");
            jedis.hset("user:1", "role", "admin");
            Map<String, String> user = jedis.hgetAll("user:1");
        }
    }
}

Explanation

All three clients speak RESP, Redis’s wire protocol, and each keeps connections alive differently: redis.Redis manages an internal pool (max_connections), ioredis pipelines commands over a single socket with built-in reconnect logic, and JedisPool hands out one blocking connection per getResource() — always inside try-with-resources or it leaks.

Two things the happy-path examples hide. First, production instances need AUTH: since Redis 6, ACLs give you named users (ACL SETUSER app on >secret ~app:* +@all), and the password rides in the URL — redis://default:secret@host:6379. Second, plain redis:// is unencrypted — over anything but loopback you want rediss:// (TLS). Managed services like ElastiCache and Redis Cloud hand you a rediss:// endpoint for exactly this reason.

flowchart diagram: App[Application

Once you’re connected and caching, the next step is Redis cache patterns — key naming, stampede protection, and invalidation strategies.

What Works

  1. Put the connection string in an env var (REDIS_URL) — credentials don’t belong in source, and the URL format works across all three drivers
  2. Set explicit expiration (EX, PX, EXAT, or KEEPTTL on rewrites) on cache keys to prevent unbounded memory growth
  3. Use decode_responses=True in Python — the default returns bytes, which breaks string comparisons silently
  4. Prefer BRPOP/BLPOP for queue consumption — blocking reads beat polling loops
  5. Batch related commands in a pipeline (.pipeline() in redis-py and Jedis, .pipeline()/.multi() in ioredis) to cut round-trips

Common Mistakes

  1. Exposing Redis on a public interface without AUTH or TLS — it’s the most-scanned default port on the internet
  2. Storing large objects (>1 MB) in single keys, causing latency spikes on every read
  3. Not handling connection failures, causing cascading app errors when Redis is down — degrade to the database, don’t crash
  4. Forgetting TTLs, leading to OOM command not allowed once maxmemory fills up
  5. Using KEYS in production — it scans every key on the single thread and blocks the instance; use SCAN instead

Advanced Examples

Python with pipelines and transactions

import redis

r = redis.Redis(host='localhost', port=6379, decode_responses=True)

# Pipeline: batch multiple commands to reduce RTT
pipe = r.pipeline()
pipe.set('user:1:name', 'Alice', ex=3600)
pipe.set('user:1:email', 'alice@example.com', ex=3600)
pipe.hset('user:1', mapping={'name': 'Alice', 'email': 'alice@example.com'})
results = pipe.execute()
print(results)  # [True, True, 1]

# Transaction with MULTI/EXEC — atomic, nothing interleaves
pipe = r.pipeline(transaction=True)
pipe.set('counter', 0)
pipe.incr('counter')
pipe.incr('counter')
results = pipe.execute()
print(results)  # [True, 1, 2]

JavaScript with sorted sets and pub/sub

const Redis = require('ioredis');
const redis = new Redis({ host: 'localhost', port: 6379 });

// Sorted sets: leaderboards
await redis.zadd('leaderboard', 100, 'alice', 85, 'bob', 120, 'charlie');
const topPlayers = await redis.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
// ['charlie', '120', 'alice', '100', 'bob', '85']

// Pub/Sub — subscriber needs its own connection
const subscriber = new Redis({ host: 'localhost', port: 6379 });
const publisher = new Redis({ host: 'localhost', port: 6379 });

await subscriber.subscribe('notifications');
subscriber.on('message', (channel, message) => {
    console.log(`Received on ${channel}: ${message}`);
});

await publisher.publish('notifications', JSON.stringify({ event: 'user_joined', userId: 42 }));

Java with JedisPool and Lua scripting

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.List;

public class RedisAdvanced {
    private final JedisPool pool;

    public RedisAdvanced(String host, int port) {
        this.pool = new JedisPool(host, port);
    }

    public void rateLimit(String userId, int maxRequests, int windowSeconds) {
        try (Jedis jedis = pool.getResource()) {
            String key = "rate:" + userId;
            String luaScript =
                "local current = redis.call('INCR', KEYS[1]) " +
                "if current == 1 then " +
                "  redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
                "end " +
                "return tonumber(current) <= tonumber(ARGV[1])";

            Object allowed = jedis.eval(
                luaScript, 1, key,
                String.valueOf(maxRequests), String.valueOf(windowSeconds)
            );
            System.out.println("Request allowed: " + allowed);
        }
    }

    public void pipelineExample() {
        try (Jedis jedis = pool.getResource()) {
            var pipe = jedis.pipelined();
            pipe.set("key1", "value1");
            pipe.set("key2", "value2");
            pipe.sync();
        }
    }
}

Python async with redis-py

import asyncio
import redis.asyncio as aioredis

async def main():
    r = aioredis.Redis(host='localhost', port=6379, decode_responses=True)

    # Async string operations
    await r.set('async:key', 'value', ex=60)
    value = await r.get('async:key')

    # Async pipeline
    pipe = r.pipeline()
    pipe.set('a', 1)
    pipe.set('b', 2)
    pipe.get('a')
    results = await pipe.execute()
    print(results)  # [True, True, '1']

    await r.aclose()

asyncio.run(main())

Variants

TechnologyDriverAsyncClusterNotes
Pythonredis-pyYes (redis.asyncio)redis.cluster.RedisClusterOfficial client — cluster support is built in since 4.x
Pythonvalkey-pyYesYesFor Valkey, the Linux Foundation fork of Redis 7.2
JavaScriptioredisYes (Promise)new Redis.Cluster()Recommended for Node.js
JavaScriptnode-redis (redis package)Yes (Promise)createCluster()Official Redis client
JavaJedisNoJedisClusterSimple blocking API
JavaLettuceYes (reactive)RedisClusterClientThe Spring Data Redis default
Gogo-redisYesClusterClientStandard Go client

Production Tips

  1. Set maxmemory and an eviction policy. Without it, Redis grows until the OOM killer picks it. allkeys-lru is the right default for caches; noeviction for anything durable:
# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
  1. Monitor with INFO and SLOWLOG. INFO shows memory, clients, and hit rates — check keyspace_hits vs keyspace_misses; SLOWLOG GET lists commands that exceeded slowlog-log-slower-than microseconds:
info = r.info()
print(f"Used memory: {info['used_memory_human']}")
print(f"Hit rate: {info['keyspace_hits'] / (info['keyspace_hits'] + info['keyspace_misses']):.1%}")
  1. Use Sentinel for high availability. Sentinel watches the master and promotes a replica on failure; all three clients have Sentinel-aware modes (redis.sentinel.Sentinel, ioredis sentinels option, JedisSentinelPool).

  2. Batch with PIPELINE, MGET, and MSET. Anything more than a few commands in a row should go in one round-trip:

values = r.mget('key1', 'key2', 'key3')
r.mset({'key1': 'val1', 'key2': 'val2'})
  1. Use HSCAN instead of HGETALL on big hashes. HGETALL copies the whole hash into memory at once; HSCAN iterates in batches:
for field, value in r.hscan_iter('large_hash', count=100):
    print(field, value)
  1. Enable lazyfree flags (Redis 4.0+). Async deletion prevents latency spikes when big keys expire or get evicted:
# redis.conf
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
  1. Inspect storage with OBJECT ENCODING. Redis switches between compact and full encodings per key — checking them tells you if a data type is bigger than it should be:
encoding = r.object('encoding', 'mykey')
print(f"Encoding: {encoding}")  # e.g., listpack, hashtable, int

Troubleshooting

ErrorWhat it meansHow to fix
ECONNREFUSEDNothing listening on 6379, or firewall/SG blocks itCheck redis-server is up, the port is open, and the client host is allowed
NOAUTH Authentication requiredThe instance has a password and you didn’t send onePass it in the URL: redis://:password@host:6379
WRONGPASS invalid username-password pairBad ACL credentials (Redis 6+)Check the user (default unless customized) and password
READONLY You can't write against a read only replicaYou’re writing to a replicaPoint writes at the master, or check whether Sentinel just failed over
OOM command not allowedmaxmemory reached and policy is noevictionRaise maxmemory or switch to an eviction policy like allkeys-lru
MISCONF Redis is configured to save RDB snapshotsPersistence to disk is failing (usually disk full or permissions)Free disk space or fix dir permissions — writes stay blocked until it’s fixed
WRONGTYPE Operation against a key holding the wrong kind of valueThe key exists with a different typeCheck TYPE key — you can’t GET a hash or HGETALL a string
Connection timed outNetwork drop or the server is busy on a slow commandLook at SLOWLOG, raise socket_timeout, and keep commands O(log n)

See Also

  • Redis documentation — commands, data types, ACLs, and client-side caching
  • ioredis — cluster, Sentinel, TLS, and retry options for Node.js
  • redis-py — connection pools, redis.asyncio, and redis.cluster
  • Jedis — JedisPooled, Sentinel, and cluster modes for Java

Frequently Asked Questions

Should I use Redis as my primary database?

Not by itself. Redis lives in memory — even with RDB snapshots or AOF persistence enabled, it's designed as a fast layer next to a real database, not the system of record. Pair it with PostgreSQL or MySQL for durable data.

How do I handle Redis cluster mode?

Use cluster-aware clients: redis.cluster.RedisCluster in redis-py 4.x (the old redis-py-cluster package is deprecated and merged upstream), new Redis.Cluster() in ioredis, and JedisCluster or Lettuce's RedisClusterClient in Java. Cluster shards data across nodes, so multi-key commands only work inside the same hash tag ({user:1}:name).

What is the difference between EX and PX in Redis?

EX sets expiration in seconds, PX in milliseconds — SET k v EX 60 equals SET k v PX 60000. EXAT/PXAT take absolute timestamps instead of relative ones, and KEEPTTL preserves the existing TTL when you overwrite a key.

How do I implement a rate limiter with Redis?

Use INCR with a window EXPIRE — or the Lua script in Advanced Examples if you need it atomic across drivers:

def rate_limit(r, user_id, max_requests=100, window=60):
    key = f"rate:{user_id}"
    current = r.incr(key)
    if current == 1:
        r.expire(key, window)
    return current <= max_requests
What is the difference between MULTI/EXEC and PIPELINE?

MULTI/EXEC is a transaction — commands run atomically and nothing interleaves. PIPELINE only batches commands to save round-trips; other clients can interleave between them. Use MULTI when correctness matters (counters, transfers), PIPELINE when you're just loading data fast.

How do I use Redis Streams for message queuing?

XADD appends entries, XREAD/XREADGROUP read them back — Streams give you Kafka-lite semantics with consumer groups, pending entries, and replay:

# Producer
r.xadd('events', {'type': 'order_created', 'order_id': 123})

# Consumer
response = r.xread({'events': '0'}, block=5000, count=10)
for stream, messages in response:
    for msg_id, fields in messages:
        print(f"Event {msg_id}: {fields}")
How do I connect with AUTH and TLS?

Put both in the URL — rediss://user:password@host:6379 gives you TLS and AUTH in one string. redis-py reads it via Redis.from_url(), ioredis parses it in the constructor (add tls: {} for custom CA certs), and Jedis accepts the rediss:// URI in JedisPool/JedisPooled.