Skip to main content

Redis

Redis

Redis is an in-memory data structure store used as a database, cache, message broker, and streaming engine, known for its exceptional performance.

Overview​

  • Versions: 8, 7.4, 7.2 (default: 8)
  • Default Port: 6379
  • Cluster Support: No (Single node only)
  • Use Cases: Caching, sessions, real-time data, pub/sub messaging
  • Features: Persistence, pub/sub, data structures, Lua scripting

Key Features​

  • Blazing Fast: In-memory operations with microsecond latency
  • Rich Data Structures: Strings, lists, sets, sorted sets, hashes, streams, and more
  • Persistence: RDB snapshots saved to the add-on's disk, so data survives a stop and start
  • Pub/Sub Messaging: Real-time message broadcasting
  • Atomic Operations: All operations are atomic
  • Lua Scripting: Execute complex operations atomically
  • TTL Support: Automatic key expiration
  • Transactions: Multi-command transactions with WATCH/MULTI/EXEC
  • Streams: Log-like data structure for event sourcing

Resources​

Choose the add-on's resources on the create form:

SettingOptionsDefault
CPU (vCPU)Any number of cores, e.g. 0.5, 1, 20.5
MemoryAny amount in GB, at least the type's minimum1 GB
DiskAny amount in GB10 GB
GPU Count0-8 (0 for CPU-only)0

Creating a Redis Add-on​

  1. Navigate to Add-ons and click Create Add-on
  2. On the Create New Add-on page, select Redis as the type
  3. Choose a version (8, 7.4, or 7.2)
  4. Configure:
    • Add-on Label (required): descriptive name (e.g., "cache-server")
    • Description (optional): purpose and notes
    • Resources: CPU, memory, and disk for your workload
  5. Optionally enable automatic backups:
    • Schedule: Hourly, Daily, Weekly, or Monthly
    • Retention: number of backups to keep (1-30, default 7)
  6. Click Create Add-on

Connection Information​

Once the add-on is running, the Connection tab of the add-on details page shows the internal host (for apps), port, password, and a ready-to-use connection string. The same details are exposed to your apps via STRONGLY_SERVICES.

Connection String Format​

redis://:password@host:6379/0

Redis connection strings use password-only authentication (no username in the connection string). The password is auto-generated during add-on creation as a 32-character random secret.

Accessing Connection Details​

In STRONGLY_SERVICES, add-ons are grouped by type under services.addons, and each entry is one provisioned instance:

{
"id": "addon-abc123defg",
"name": "cache-server",
"type": "redis",
"category": "add-on",
"status": "running",
"version": "8",
"connection": {
"connection_string": "redis://:<password>@<internal-host>:6379/0",
"uri": "redis://:<password>@<internal-host>:6379/0",
"host": "<internal-host>",
"port": 6379
},
"auth": {
"method": "username_password",
"credentials": { "username": "user_a1b2c3d4", "password": "<password>" }
},
"limits": { "max_connections": 100, "storage_gb": 10 },
"metadata": { "cpu": "0.5", "memory": "1GB", "disk": "10GB", "backup_enabled": false }
}

Use only the password for authentication; Redis does not use the generated username.

import os
import json
import redis

# Parse STRONGLY_SERVICES
services = json.loads(os.environ['STRONGLY_SERVICES'])

# Pick your Redis add-on by name (the label you gave it)
redis_addon = next(
a for a in services['services']['addons']['redis']
if a['name'] == 'cache-server'
)

# Connect using the connection string
r = redis.from_url(redis_addon['connection']['connection_string'])

# Or connect using individual parameters
r = redis.Redis(
host=redis_addon['connection']['host'],
port=redis_addon['connection']['port'],
password=redis_addon['auth']['credentials']['password'],
decode_responses=True
)

# Set and get values
r.set('key', 'value')
value = r.get('key')
print(value)

Common Operations​

Basic Key-Value Operations​

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

# SET and GET
r.set('user:1000:name', 'John Doe')
name = r.get('user:1000:name')

# SET with expiration (in seconds)
r.setex('session:abc123', 3600, 'session_data')

# SET if not exists
r.setnx('lock:resource', 'locked')

# Multiple SET
r.mset({'key1': 'value1', 'key2': 'value2', 'key3': 'value3'})

# Multiple GET
values = r.mget(['key1', 'key2', 'key3'])

# INCREMENT and DECREMENT
r.incr('page:views')
r.incrby('score', 10)
r.decr('inventory:item:123')

# DELETE
r.delete('key1')

# Check existence
exists = r.exists('key1')

# Set expiration
r.expire('key1', 300) # 300 seconds

# Get TTL
ttl = r.ttl('key1')

Lists​

# Push to list
r.lpush('queue:tasks', 'task1', 'task2', 'task3') # Left push
r.rpush('queue:tasks', 'task4') # Right push

# Pop from list
task = r.lpop('queue:tasks') # Left pop
task = r.rpop('queue:tasks') # Right pop

# Blocking pop (wait for item)
task = r.blpop('queue:tasks', timeout=5)

# Get list length
length = r.llen('queue:tasks')

# Get range
tasks = r.lrange('queue:tasks', 0, -1) # All items

# Trim list
r.ltrim('queue:tasks', 0, 99) # Keep first 100 items

Sets​

# Add members to set
r.sadd('tags:post:1', 'python', 'redis', 'database')

# Check membership
is_member = r.sismember('tags:post:1', 'python')

# Get all members
tags = r.smembers('tags:post:1')

# Remove member
r.srem('tags:post:1', 'database')

# Set operations
r.sadd('set1', 'a', 'b', 'c')
r.sadd('set2', 'b', 'c', 'd')

union = r.sunion('set1', 'set2') # {'a', 'b', 'c', 'd'}
inter = r.sinter('set1', 'set2') # {'b', 'c'}
diff = r.sdiff('set1', 'set2') # {'a'}

# Random member
random_tag = r.srandmember('tags:post:1')

# Pop random member
tag = r.spop('tags:post:1')

Sorted Sets (Leaderboards)​

# Add members with scores
r.zadd('leaderboard', {'player1': 100, 'player2': 150, 'player3': 120})

# Increment score
r.zincrby('leaderboard', 10, 'player1')

# Get rank (0-based)
rank = r.zrank('leaderboard', 'player1')
reverse_rank = r.zrevrank('leaderboard', 'player1') # Highest score = rank 0

# Get score
score = r.zscore('leaderboard', 'player1')

# Get top N players
top_players = r.zrevrange('leaderboard', 0, 9, withscores=True)

# Get players by score range
players = r.zrangebyscore('leaderboard', 100, 200, withscores=True)

# Count members in score range
count = r.zcount('leaderboard', 100, 200)

# Remove member
r.zrem('leaderboard', 'player1')

Hashes​

# Set hash fields
r.hset('user:1000', mapping={
'name': 'John Doe',
'email': 'john@example.com',
'age': '30'
})

# Get single field
name = r.hget('user:1000', 'name')

# Get all fields
user = r.hgetall('user:1000')

# Get multiple fields
fields = r.hmget('user:1000', ['name', 'email'])

# Increment hash field
r.hincrby('user:1000', 'login_count', 1)

# Check field existence
exists = r.hexists('user:1000', 'name')

# Get all keys
keys = r.hkeys('user:1000')

# Get all values
values = r.hvals('user:1000')

# Delete field
r.hdel('user:1000', 'age')

Pub/Sub Messaging​

# Publisher
import redis
r = redis.Redis(host='host', port=6379)
r.publish('channel:notifications', 'Hello, World!')

# Subscriber
p = r.pubsub()
p.subscribe('channel:notifications')

for message in p.listen():
if message['type'] == 'message':
print(message['data'])

# Pattern subscription
p.psubscribe('channel:*')

# Unsubscribe
p.unsubscribe('channel:notifications')

Transactions​

# MULTI/EXEC transaction
pipe = r.pipeline()
pipe.set('key1', 'value1')
pipe.set('key2', 'value2')
pipe.incr('counter')
results = pipe.execute()

# Optimistic locking with WATCH
pipe = r.pipeline()
while True:
try:
# Watch key for changes
pipe.watch('balance:1000')

current_balance = int(r.get('balance:1000'))
if current_balance >= 100:
# Start transaction
pipe.multi()
pipe.decrby('balance:1000', 100)
pipe.incrby('balance:2000', 100)
pipe.execute()
break
else:
pipe.unwatch()
break
except redis.WatchError:
# Key was modified, retry
continue

Streams​

# Add to stream
r.xadd('events', {'user': 'john', 'action': 'login'})

# Read from stream
messages = r.xread({'events': '0'}, count=10)

# Consumer groups
r.xgroup_create('events', 'mygroup', id='0')
messages = r.xreadgroup('mygroup', 'consumer1', {'events': '>'}, count=10)

# Acknowledge message
r.xack('events', 'mygroup', message_id)

Caching Patterns​

Cache-Aside Pattern​

def get_user(user_id):
# Try cache first
cache_key = f'user:{user_id}'
cached = r.get(cache_key)

if cached:
return json.loads(cached)

# Cache miss - get from database
user = db.query("SELECT * FROM users WHERE id = ?", user_id)

# Store in cache with 1 hour TTL
r.setex(cache_key, 3600, json.dumps(user))

return user

Write-Through Cache​

def update_user(user_id, data):
# Update database
db.query("UPDATE users SET ... WHERE id = ?", user_id, data)

# Update cache
cache_key = f'user:{user_id}'
r.setex(cache_key, 3600, json.dumps(data))

Cache Invalidation​

# Delete specific key
r.delete(f'user:{user_id}')

# Delete pattern
for key in r.scan_iter('user:*'):
r.delete(key)

# Set TTL on existing key
r.expire(f'user:{user_id}', 60)

Session Management​

from flask import Flask, session
from flask_session import Session
import redis

app = Flask(__name__)
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_REDIS'] = redis.from_url(redis_url)
Session(app)

@app.route('/login')
def login():
session['user_id'] = 1000
session['username'] = 'johndoe'
return 'Logged in'

@app.route('/profile')
def profile():
user_id = session.get('user_id')
return f'User: {user_id}'

Rate Limiting​

def is_rate_limited(user_id, limit=100, window=60):
"""
Allow 'limit' requests per 'window' seconds
"""
key = f'rate_limit:{user_id}'
current = r.incr(key)

if current == 1:
r.expire(key, window)

return current > limit

# Usage
if is_rate_limited('user:1000', limit=10, window=60):
return 'Rate limit exceeded', 429

Distributed Locking​

import time
import uuid

def acquire_lock(lock_name, timeout=10):
"""
Acquire distributed lock with automatic expiration
"""
lock_key = f'lock:{lock_name}'
identifier = str(uuid.uuid4())

end = time.time() + timeout
while time.time() < end:
if r.set(lock_key, identifier, nx=True, ex=timeout):
return identifier
time.sleep(0.001)

return False

def release_lock(lock_name, identifier):
"""
Release distributed lock
"""
lock_key = f'lock:{lock_name}'
pipe = r.pipeline(True)

while True:
try:
pipe.watch(lock_key)
if pipe.get(lock_key) == identifier:
pipe.multi()
pipe.delete(lock_key)
pipe.execute()
return True
pipe.unwatch()
break
except redis.WatchError:
pass

return False

# Usage
lock_id = acquire_lock('resource:123')
if lock_id:
try:
# Critical section
pass
finally:
release_lock('resource:123', lock_id)

Backups​

A Redis backup is an RDB snapshot of the dataset (backup.rdb).

  • Back up now: click Backup Now on the status card, or Back Up Now on the Backup tab, while the add-on is running.

  • Automatic: on the Backup tab turn on Enable Automatic Backups, choose a Backup Schedule (Hourly, Daily, Weekly or Monthly) and a Retention (3, 7, 14 or 30 backups), and click Save Configuration. Older backups beyond the retention count are deleted automatically.

  • History: the Backup tab lists every backup with its status, size and any error.

  • Restore: click Restore next to a succeeded backup in Backup History and confirm. The backup is loaded back into this add-on while it keeps running: every key is removed and the keys in the backup are loaded with their expiry times. Reads miss keys until the restore finishes. Data written after the backup is lost. See Restoring a backup.

Performance Optimization​

Connection Pooling​

import redis

# Connection pooling is built-in
pool = redis.ConnectionPool(
host='host',
port=6379,
password='password',
max_connections=50,
decode_responses=True
)

r = redis.Redis(connection_pool=pool)

Pipeline Commands​

# Without pipeline (multiple round-trips)
for i in range(1000):
r.set(f'key:{i}', f'value:{i}')

# With pipeline (single round-trip)
pipe = r.pipeline()
for i in range(1000):
pipe.set(f'key:{i}', f'value:{i}')
pipe.execute()

Memory Optimization​

# Use hashes for multiple related fields (more memory efficient)
# Instead of:
r.set('user:1000:name', 'John')
r.set('user:1000:email', 'john@example.com')
r.set('user:1000:age', '30')

# Use:
r.hset('user:1000', mapping={
'name': 'John',
'email': 'john@example.com',
'age': '30'
})

# Redis eviction behavior is controlled by maxmemory-policy.
# maxmemory-policy options:
# - noeviction: Return errors when memory limit is reached
# - allkeys-lru: Evict least recently used keys
# - volatile-lru: Evict least recently used keys with TTL
# - allkeys-random: Evict random keys
# - volatile-random: Evict random keys with TTL
# - volatile-ttl: Evict keys with nearest expiration

Monitoring​

The Metrics tab on the add-on details page measures the running add-on live: CPU, memory and disk use against its size, network traffic, open and new connections, response time, and instance health and uptime. See Metrics. The Logs tab shows its recent log output.

Redis INFO Command​

# Get all server information
info = r.info()

# Specific sections
memory_info = r.info('memory')
stats = r.info('stats')
replication = r.info('replication')

# Key metrics
print(f"Used Memory: {info['used_memory_human']}")
print(f"Connected Clients: {info['connected_clients']}")
print(f"Total Commands: {info['total_commands_processed']}")
print(f"Keyspace Hits: {info['keyspace_hits']}")
print(f"Keyspace Misses: {info['keyspace_misses']}")

# Calculate hit rate
hit_rate = info['keyspace_hits'] / (info['keyspace_hits'] + info['keyspace_misses'])
print(f"Hit Rate: {hit_rate:.2%}")

Best Practices​

  1. Use Connection Pooling: Reuse connections for better performance
  2. Set Appropriate TTLs: Prevent memory overflow with expiration
  3. Use Pipelines: Batch commands to reduce network overhead
  4. Choose Right Data Structure: Use the most appropriate data type for your use case
  5. Monitor Memory Usage: Redis is in-memory, watch your memory consumption
  6. Use Hashes for Objects: More memory-efficient than individual keys
  7. Implement Cache Invalidation: Keep cache consistent with source of truth
  8. Know the Persistence Window: the add-on saves RDB snapshots periodically (Redis's default save rules), so the most recent writes can be lost if it stops unexpectedly. Keep data you cannot lose in a database add-on
  9. Use Transactions Wisely: For atomic multi-step operations
  10. Avoid Large Keys: Break large collections into smaller chunks
  11. Set Eviction Policy: Configure appropriate maxmemory-policy

Troubleshooting​

Connection Issues​

# Test connection
try:
r.ping()
print("Connected to Redis")
except redis.ConnectionError:
print("Cannot connect to Redis")

Memory Issues​

# Check memory usage
info = r.info('memory')
print(f"Used Memory: {info['used_memory_human']}")
print(f"Peak Memory: {info['used_memory_peak_human']}")
print(f"Memory Fragmentation: {info['mem_fragmentation_ratio']}")

# Find large keys
for key in r.scan_iter():
key_type = r.type(key)
if key_type == 'string':
size = len(r.get(key))
elif key_type == 'list':
size = r.llen(key)
elif key_type == 'set':
size = r.scard(key)
elif key_type == 'zset':
size = r.zcard(key)
elif key_type == 'hash':
size = r.hlen(key)

if size > 10000: # Arbitrary threshold
print(f"Large key: {key} ({key_type}) - {size}")

Performance Issues​

# Check slow log
slowlog = r.slowlog_get(10)
for entry in slowlog:
print(f"Duration: {entry['duration']}us, Command: {entry['command']}")

# Monitor commands in real-time
# Use redis-cli MONITOR (not in production - high overhead)

Support​

For issues or questions:

  • Check add-on logs in the Logs tab of the add-on details page
  • Review Redis official documentation
  • Contact Strongly support through the platform