Skip to main content
Bytes & Beyond

Redis Strings

String data type in Redis - the foundation for caching, counters, sessions, and more.

What are Strings?

A String is the simplest Redis data type, just bytes. It can hold text, numbers, binary data, JSON, or anything else. Every value in Redis is ultimately stored as a string, but this is the basic building block.

Basic Commands

SET key "value"           # Store a string
GET key                   # Get the string
APPEND key " more"        # Add to the end
STRLEN key                # How long is it?
GETRANGE key 0 4          # Get part of it (chars 0-4)
SETRANGE key 0 "new"      # Replace part of it

Working with Numbers

Strings can hold numbers, and Redis gives you special commands to work with them:

SET counter 10
INCR counter              # Increment by 1 → 11
DECR counter              # Decrement by 1 → 10
INCRBY counter 5          # Add 5 → 15
DECRBY counter 3          # Subtract 3 → 12
INCRBYFLOAT price 0.99    # Add decimal → 12.99

Use Cases

Session Tokens

Store user session data:

SET session:user:1 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
SET session:user:2 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Check if session exists
GET session:user:1

Node.js (ioredis):

import Redis from 'ioredis';
const redis = new Redis();

// Store session
await redis.set(`session:user:${userId}`, token, 'EX', 86400); // 24 hours

// Get session
const session = await redis.get(`session:user:${userId}`);
if (!session) {
  throw new Error('Session expired');
}

Python (redis-py):

import redis

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

# Store session
def store_session(user_id: int, token: str):
    r.set(f"session:user:{user_id}", token, ex=86400)  # 24 hours

# Get session
def get_session(user_id: int) -> str | None:
    session = r.get(f"session:user:{user_id}")
    return session  # Returns None if expired/not found

# Usage
store_session(1, "eyJhbGciOiJIUzI1NiIs...")
token = get_session(1)
if not token:
    print("Session expired")

Page View Counters

Track how many times something is viewed:

SET page:views 0
INCR page:views           # First visit: 1
INCR page:views           # Second visit: 2
INCRBY page:views 100     # Bulk add 100 views

# Get total views
GET page:views            # 102

Node.js:

// Increment page view
await redis.incr(`page:${pageId}:views`);

// Get view count
const views = await redis.get(`page:${pageId}:views`);
console.log(`Page has ${views} views`);

Python:

# Increment page view
def increment_page_view(page_id: int) -> int:
    return r.incr(f"page:{page_id}:views")

# Get view count
def get_page_views(page_id: int) -> int:
    views = r.get(f"page:{page_id}:views")
    return int(views or 0)

# Usage
increment_page_view(1)
print(f"Page has {get_page_views(1)} views")

Rate Limiting

Limit API requests per user:

SET api:requests:user:1 0
INCR api:requests:user:1  # First request
INCR api:requests:user:1  # Second request

# Get current count
count = GET api:requests:user:1

if count > 100:
  REJECT("Rate limit exceeded")

Node.js:

async function checkRateLimit(userId, limit = 100) {
  const key = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`; // Per minute
  
  const count = await redis.incr(key);
  if (count === 1) {
    await redis.expire(key, 60); // Expire after 60 seconds
  }
  
  if (count > limit) {
    throw new Error('Rate limit exceeded');
  }
  return count;
}

Python:

import time

def check_rate_limit(user_id: str, limit: int = 100) -> tuple[bool, int]:
    """Returns (is_allowed, current_count)"""
    key = f"ratelimit:{user_id}:{int(time.time() // 60)}"  # Per minute
    
    count = r.incr(key)
    if count == 1:
        r.expire(key, 60)
    
    return count <= limit, count

# Usage
is_allowed, count = check_rate_limit("user:123")
if not is_allowed:
    print(f"Rate limit exceeded: {count}/100")

Storing JSON Objects

Cache entire API responses as JSON:

SET user:profile:1 '{"id":1,"name":"Alice","email":"alice@example.com","age":25}'
SET product:1 '{"name":"Laptop","price":999,"stock":5,"category":"electronics"}'

# Later retrieve
GET user:profile:1
GET product:1

Node.js:

// Store JSON
const user = { id: 1, name: 'Alice', email: 'alice@example.com' };
await redis.set(`user:${user.id}`, JSON.stringify(user), 'EX', 3600);

// Retrieve and parse
const cached = await redis.get(`user:${userId}`);
if (cached) {
  return JSON.parse(cached);
}

Python:

import json

# Store JSON
def cache_user(user: dict, ttl: int = 3600):
    r.set(f"user:{user['id']}", json.dumps(user), ex=ttl)

# Retrieve and parse
def get_cached_user(user_id: int) -> dict | None:
    cached = r.get(f"user:{user_id}")
    return json.loads(cached) if cached else None

# Usage
user = {"id": 1, "name": "Alice", "email": "alice@example.com"}
cache_user(user)
retrieved = get_cached_user(1)

HTML Page Caching

Cache rendered HTML pages:

SET page:home "<html><body>...</body></html>"
SET page:about "<html><body>...</body></html>"

# Get cached page
GET page:home

Node.js (HTML Caching Middleware):

// Middleware to cache rendered pages
async function htmlCacheMiddleware(req, res, next) {
  const cacheKey = `page:${req.path}`;
  
  // Check cache first
  const cachedHtml = await redis.get(cacheKey);
  if (cachedHtml) {
    res.setHeader('X-Cache', 'HIT');
    return res.send(cachedHtml);
  }
  
  // Store original send
  const originalSend = res.send.bind(res);
  
  res.send = async (html) => {
    // Cache for 10 minutes
    await redis.set(cacheKey, html, 'EX', 600);
    res.setHeader('X-Cache', 'MISS');
    return originalSend(html);
  };
  
  next();
}

Python:

def get_cached_page(page_name: str) -> tuple[str | None, bool]:
    """Returns (html_content, is_cache_hit)"""
    cache_key = f"page:{page_name}"
    cached_html = r.get(cache_key)
    
    if cached_html:
        return cached_html, True
    return None, False

def cache_page(page_name: str, html: str, ttl: int = 600):
    """Cache HTML for 10 minutes by default"""
    r.set(f"page:{page_name}", html, ex=ttl)

# Usage
html, hit = get_cached_page("home")
if not hit:
    html = render_page("home")  # Your render function
    cache_page("home", html)

User Preferences

Store configuration or settings:

SET theme:user:1 "dark"
SET language:user:1 "en"
SET timezone:user:1 "UTC"

# Update a setting
SET theme:user:1 "light"

Node.js (User Preferences):

async function getUserPreferences(userId) {
  const [theme, language, timezone] = await Promise.all([
    redis.get(`theme:user:${userId}`),
    redis.get(`language:user:${userId}`),
    redis.get(`timezone:user:${userId}`)
  ]);
  
  return {
    theme: theme || 'light',
    language: language || 'en',
    timezone: timezone || 'UTC'
  };
}

async function updateUserPreference(userId, key, value) {
  await redis.set(`${key}:user:${userId}`, value);
}

// Usage
await updateUserPreference(1, 'theme', 'dark');
const prefs = await getUserPreferences(1);

Python:

def get_user_preferences(user_id: int) -> dict:
    theme = r.get(f"theme:user:{user_id}")
    language = r.get(f"language:user:{user_id}")
    timezone = r.get(f"timezone:user:{user_id}")
    
    return {
        "theme": theme or "light",
        "language": language or "en",
        "timezone": timezone or "UTC"
    }

def update_user_preference(user_id: int, key: str, value: str):
    r.set(f"{key}:user:{user_id}", value)

# Usage
update_user_preference(1, "theme", "dark")
prefs = get_user_preferences(1)
print(prefs)  # {'theme': 'dark', 'language': 'en', 'timezone': 'UTC'}

Strings with Expiration

All strings can expire automatically (TTL):

SET temp_token "abc123" EX 3600      # Expires in 1 hour
SET cache_key "data" PX 5000         # Expires in 5000 milliseconds
SET session:user:1 "data" EX 86400   # Expires in 1 day

# Check how much time is left
TTL session:user:1                   # Seconds remaining
PTTL session:user:1                  # Milliseconds remaining

Perfect for:

  • Session tokens that expire
  • OTP codes (valid for 5 minutes)
  • Cache that needs refreshing
  • Temporary locks

Get and Set in One Operation

Sometimes you need to set a new value but keep the old one:

SET counter 10
GETSET counter 20                    # Returns old value (10), sets to 20
GET counter                          # Now it's 20

Or set only if the key doesn’t exist:

SETNX key "value"                  # Set if Not eXists
# If key exists, nothing happens

Batch Operations

Work with multiple strings at once:

MSET key1 "value1" key2 "value2" key3 "value3"
MGET key1 key2 key3                  # Returns all three values

Common Patterns

Counter Pattern

Track anything that increments:

# Page views
INCR page:1:views

# API call count
INCR api:calls:today

# Post likes
INCR post:123:likes

# Session active count
INCR users:online

Node.js (Counter Examples):

// Like a post
async function likePost(postId, userId) {
  const likesKey = `post:${postId}:likes`;
  const userLikedKey = `post:${postId}:liked:${userId}`;
  
  // Check if user already liked
  const alreadyLiked = await redis.exists(userLikedKey);
  if (alreadyLiked) {
    return { success: false, message: 'Already liked' };
  }
  
  // Increment likes and mark user as liked
  const [newCount] = await redis.multi()
    .incr(likesKey)
    .set(userLikedKey, '1')
    .exec();
  
  return { success: true, likes: newCount };
}

// Track daily API calls
async function trackApiCall(endpoint) {
  const today = new Date().toISOString().split('T')[0];
  const key = `api:calls:${endpoint}:${today}`;
  
  const count = await redis.incr(key);
  await redis.expire(key, 86400 * 7); // Keep for 7 days
  
  return count;
}

Python:

from datetime import date

def like_post(post_id: int, user_id: int) -> dict:
    likes_key = f"post:{post_id}:likes"
    user_liked_key = f"post:{post_id}:liked:{user_id}"
    
    # Check if user already liked
    if r.exists(user_liked_key):
        return {"success": False, "message": "Already liked"}
    
    # Increment likes and mark user as liked (atomic pipeline)
    pipe = r.pipeline()
    pipe.incr(likes_key)
    pipe.set(user_liked_key, "1")
    results = pipe.execute()
    
    return {"success": True, "likes": results[0]}

def track_api_call(endpoint: str) -> int:
    today = date.today().isoformat()
    key = f"api:calls:{endpoint}:{today}"
    
    count = r.incr(key)
    r.expire(key, 86400 * 7)  # Keep for 7 days
    
    return count

# Usage
result = like_post(123, 1)
count = track_api_call("/api/users")

Cache Pattern

Cache expensive operations:

# First request - not in cache
user = GET user:1:profile
if user is empty:
  user = database.query("SELECT * FROM users WHERE id=1")
  SET user:1:profile user
  SET user:1:profile EX 3600         # Expire in 1 hour

# Second request - instant from cache
user = GET user:1:profile             # Returns instantly

Node.js (Cache-Aside Pattern):

async function getUserById(userId) {
  const cacheKey = `user:${userId}`;
  
  // Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return JSON.parse(cached);
  }
  
  // Cache miss - fetch from database
  const user = await db.query('SELECT * FROM users WHERE id = ?', [userId]);
  
  // Store in cache for 1 hour
  await redis.set(cacheKey, JSON.stringify(user), 'EX', 3600);
  
  return user;
}

Python:

def get_user_by_id(user_id: int, db_fetch_func) -> dict | None:
    cache_key = f"user:{user_id}"
    
    # Try cache first
    cached = r.get(cache_key)
    if cached:
        return json.loads(cached)
    
    # Cache miss - fetch from database
    user = db_fetch_func(user_id)  # Your database function
    
    if user:
        # Store in cache for 1 hour
        r.set(cache_key, json.dumps(user), ex=3600)
    
    return user

# Usage
user = get_user_by_id(1, lambda uid: db.query(f"SELECT * FROM users WHERE id={uid}"))

Distributed Lock Pattern

Simple locks across multiple servers:

# Try to acquire lock
SET lock:resource:1 "locked" NX EX 10  # Only if not exists, expire in 10s
# NX = only set if key doesn't exist

# Lock acquired, do work...

# Release lock
DEL lock:resource:1

Node.js (Distributed Lock):

async function acquireLock(resourceId, ttl = 10) {
  const lockKey = `lock:resource:${resourceId}`;
  const lockId = crypto.randomUUID(); // Unique lock identifier
  
  // Try to acquire lock with NX (only if not exists)
  const acquired = await redis.set(lockKey, lockId, 'NX', 'EX', ttl);
  
  if (acquired) {
    return { success: true, lockId };
  }
  return { success: false };
}

async function releaseLock(resourceId, lockId) {
  const lockKey = `lock:resource:${resourceId}`;
  
  // Only release if we own the lock
  const currentLock = await redis.get(lockKey);
  if (currentLock === lockId) {
    await redis.del(lockKey);
    return true;
  }
  return false;
}

// Usage
const { success, lockId } = await acquireLock('order:123');
if (success) {
  try {
    // Do critical work...
    await processOrder(123);
  } finally {
    await releaseLock('order:123', lockId);
  }
}

Python:

import uuid

def acquire_lock(resource_id: str, ttl: int = 10) -> tuple[bool, str]:
    """Try to acquire a distributed lock. Returns (success, lock_id)"""
    lock_key = f"lock:resource:{resource_id}"
    lock_id = str(uuid.uuid4())  # Unique lock identifier
    
    # Try to acquire lock with NX (only if not exists)
    acquired = r.set(lock_key, lock_id, nx=True, ex=ttl)
    
    if acquired:
        return True, lock_id
    return False, ""

def release_lock(resource_id: str, lock_id: str) -> bool:
    """Release lock only if we own it"""
    lock_key = f"lock:resource:{resource_id}"
    
    # Only release if we own the lock
    current_lock = r.get(lock_key)
    if current_lock and current_lock == lock_id:
        r.delete(lock_key)
        return True
    return False

# Usage
success, lock_id = acquire_lock("order:123")
if success:
    try:
        # Do critical work...
        process_order(123)
    finally:
        release_lock("order:123", lock_id)
else:
    print("Resource is locked by another process")