Skip to main content
Bytes & Beyond

Redis Hashes

Hashes in Redis - perfect for storing objects with multiple fields like user profiles and product data.

What are Hashes?

A Hash is like a JavaScript object or dictionary — a collection of field-value pairs. Perfect for storing related data together, like a user profile or product details.

Think of it as an object inside Redis.

Basic Commands

HSET user:1 name "Alice" age 25 city "NYC"

HGET user:1 name          # Get one field: "Alice"
HGETALL user:1            # Get all fields and values
HDEL user:1 city          # Delete city field
HEXISTS user:1 name       # Does name field exist? Yes
HKEYS user:1              # All field names: ["name", "age", "city"]
HVALS user:1              # All values: ["Alice", 25, "NYC"]
HLEN user:1               # Number of fields: 3

Multiple Fields at Once

# Set multiple fields
HMSET product:101 name "Laptop" price 999 stock 5
# or
HSET product:101 name "Laptop" price 999 stock 5

# Get multiple fields
HMGET product:101 name price
# Returns: ["Laptop", 999]

Updating and Incrementing

HSET user:1 name "Alice" age 25

# Update a field
HSET user:1 age 26

# Increment a field
HINCRBY user:1 age 1      # Now age is 27
HINCRBYFLOAT product:1 price 9.99  # Add decimal to price

Real-world: User Profiles

HSET user:1 \
  username "alice" \
  email "alice@example.com" \
  first_name "Alice" \
  last_name "Smith" \
  joined "2024-01-15" \
  avatar "https://example.com/alice.jpg" \
  verified true \
  bio "Developer, coffee enthusiast"

# Retrieve full profile
HGETALL user:1

# Get just email for verification
HGET user:1 email

# Update avatar
HSET user:1 avatar "https://example.com/alice-new.jpg"

# Check if verified
HEXISTS user:1 verified

Node.js (ioredis):

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

// Create user profile
async function createUserProfile(userId, profile) {
  await redis.hset(`user:${userId}`, {
    username: profile.username,
    email: profile.email,
    first_name: profile.firstName,
    last_name: profile.lastName,
    joined: new Date().toISOString(),
    avatar: profile.avatar || '',
    verified: 'false',
    bio: profile.bio || ''
  });
}

// Get full profile
async function getUserProfile(userId) {
  const profile = await redis.hgetall(`user:${userId}`);
  if (!Object.keys(profile).length) return null;
  return profile;
}

// Get specific fields
async function getUserEmail(userId) {
  return await redis.hget(`user:${userId}`, 'email');
}

// Update specific fields
async function updateUserProfile(userId, updates) {
  await redis.hset(`user:${userId}`, updates);
}

// Usage
await createUserProfile(1, { username: 'alice', email: 'alice@example.com', firstName: 'Alice', lastName: 'Smith' });
const profile = await getUserProfile(1);
await updateUserProfile(1, { avatar: 'https://example.com/alice-new.jpg', bio: 'Updated bio' });

Python (redis-py):

import redis
from datetime import datetime

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

def create_user_profile(user_id: int, profile: dict):
    r.hset(f"user:{user_id}", mapping={
        'username': profile['username'],
        'email': profile['email'],
        'first_name': profile.get('first_name', ''),
        'last_name': profile.get('last_name', ''),
        'joined': datetime.now().isoformat(),
        'avatar': profile.get('avatar', ''),
        'verified': 'false',
        'bio': profile.get('bio', '')
    })

def get_user_profile(user_id: int) -> dict | None:
    profile = r.hgetall(f"user:{user_id}")
    return profile if profile else None

def update_user_profile(user_id: int, updates: dict):
    r.hset(f"user:{user_id}", mapping=updates)

# Usage
create_user_profile(1, {'username': 'alice', 'email': 'alice@example.com'})
profile = get_user_profile(1)
update_user_profile(1, {'avatar': 'https://example.com/alice.jpg'})

Real-world: Product Inventory

HSET product:1001 \
  name "MacBook Pro" \
  price 1999.99 \
  stock 5 \
  category "electronics" \
  sku "MB-2024-001" \
  description "14-inch laptop" \
  rating 4.8

# Display product
name = HGET product:1001 name          # "MacBook Pro"
price = HGET product:1001 price        # 1999.99
available = HGET product:1001 stock > 0

# Someone buys one
HINCRBY product:1001 stock -1          # Decrease stock by 1

# Check new price after sale
HINCRBYFLOAT product:1001 price -99.99  # Apply discount

Node.js (ioredis):

// Create product
async function createProduct(productId, product) {
  await redis.hset(`product:${productId}`, {
    name: product.name,
    price: product.price.toString(),
    stock: product.stock.toString(),
    category: product.category,
    sku: product.sku,
    description: product.description,
    rating: product.rating?.toString() || '0'
  });
}

// Get product for display
async function getProduct(productId) {
  const product = await redis.hgetall(`product:${productId}`);
  if (!Object.keys(product).length) return null;
  
  return {
    ...product,
    price: parseFloat(product.price),
    stock: parseInt(product.stock),
    rating: parseFloat(product.rating)
  };
}

// Purchase product (atomic stock decrease)
async function purchaseProduct(productId, quantity = 1) {
  const stock = await redis.hget(`product:${productId}`, 'stock');
  
  if (parseInt(stock) < quantity) {
    throw new Error('Insufficient stock');
  }
  
  const newStock = await redis.hincrby(`product:${productId}`, 'stock', -quantity);
  return { success: true, remainingStock: newStock };
}

// Apply discount
async function applyDiscount(productId, discountAmount) {
  const newPrice = await redis.hincrbyfloat(`product:${productId}`, 'price', -discountAmount);
  return { newPrice: parseFloat(newPrice) };
}

Python (redis-py):

def create_product(product_id: int, product: dict):
    r.hset(f"product:{product_id}", mapping={
        'name': product['name'],
        'price': str(product['price']),
        'stock': str(product['stock']),
        'category': product['category'],
        'sku': product['sku'],
        'description': product.get('description', ''),
        'rating': str(product.get('rating', 0))
    })

def get_product(product_id: int) -> dict | None:
    product = r.hgetall(f"product:{product_id}")
    if not product:
        return None
    
    return {
        **product,
        'price': float(product['price']),
        'stock': int(product['stock']),
        'rating': float(product['rating'])
    }

def purchase_product(product_id: int, quantity: int = 1) -> dict:
    stock = r.hget(f"product:{product_id}", 'stock')
    
    if int(stock) < quantity:
        raise ValueError("Insufficient stock")
    
    new_stock = r.hincrby(f"product:{product_id}", 'stock', -quantity)
    return {'remaining_stock': new_stock}

# Usage
create_product(1001, {'name': 'MacBook Pro', 'price': 1999.99, 'stock': 5, 'category': 'electronics', 'sku': 'MB-2024'})
product = get_product(1001)
purchase_product(1001, 1)

Real-world: Game Leaderboard Entry

HSET leaderboard:user:1 \
  username "alice" \
  score 2500 \
  level 15 \
  wins 42 \
  losses 8 \
  last_played "2026-02-09"

# Get player info
HGETALL leaderboard:user:1

# Update score after win
HINCRBY leaderboard:user:1 score 150
HINCRBY leaderboard:user:1 wins 1

Real-world: Session Data

HSET session:abc123def456 \
  user_id "user:1" \
  username "alice" \
  login_time "2026-02-09T10:00:00Z" \
  last_activity "2026-02-09T10:15:00Z" \
  ip_address "192.168.1.1" \
  user_agent "Chrome/120"

# Check session validity
HEXISTS session:abc123def456 user_id  # Is valid

# Update last activity
HSET session:abc123def456 last_activity "2026-02-09T10:20:00Z"

# Get user from session
HGET session:abc123def456 user_id

Node.js (ioredis):

import crypto from 'crypto';

// Create session
async function createSession(userId, username, req) {
  const sessionId = crypto.randomBytes(32).toString('hex');
  const now = new Date().toISOString();
  
  await redis.hset(`session:${sessionId}`, {
    user_id: userId,
    username: username,
    login_time: now,
    last_activity: now,
    ip_address: req.ip,
    user_agent: req.headers['user-agent']
  });
  
  await redis.expire(`session:${sessionId}`, 86400); // 24 hours
  return sessionId;
}

// Validate and refresh session
async function validateSession(sessionId) {
  const exists = await redis.hexists(`session:${sessionId}`, 'user_id');
  if (!exists) return null;
  
  // Update last activity and refresh TTL
  await redis.hset(`session:${sessionId}`, 'last_activity', new Date().toISOString());
  await redis.expire(`session:${sessionId}`, 86400);
  
  return await redis.hgetall(`session:${sessionId}`);
}

// Destroy session
async function destroySession(sessionId) {
  await redis.del(`session:${sessionId}`);
}

Python (redis-py):

import secrets
from datetime import datetime

def create_session(user_id: str, username: str, ip_address: str, user_agent: str = '') -> str:
    session_id = secrets.token_hex(32)
    now = datetime.now().isoformat()
    
    r.hset(f"session:{session_id}", mapping={
        'user_id': user_id,
        'username': username,
        'login_time': now,
        'last_activity': now,
        'ip_address': ip_address,
        'user_agent': user_agent
    })
    
    r.expire(f"session:{session_id}", 86400)  # 24 hours
    return session_id

def validate_session(session_id: str) -> dict | None:
    if not r.hexists(f"session:{session_id}", 'user_id'):
        return None
    
    # Update last activity and refresh TTL
    r.hset(f"session:{session_id}", 'last_activity', datetime.now().isoformat())
    r.expire(f"session:{session_id}", 86400)
    
    return r.hgetall(f"session:{session_id}")

def destroy_session(session_id: str):
    r.delete(f"session:{session_id}")

# Usage
session_id = create_session("user:1", "alice", "192.168.1.1", "Chrome/120")
session = validate_session(session_id)
destroy_session(session_id)

Real-world: Configuration

HSET config:app \
  name "MyApp" \
  version "1.2.3" \
  max_connections 1000 \
  timeout 30 \
  debug false \
  theme "dark"

# Get config
HGET config:app theme  # "dark"

# Update config
HSET config:app theme "light"

Efficiency: Hash vs Multiple Strings

Using Hashes (better):

HSET user:1 name "Alice" age 25 email "alice@example.com"

# One key, efficient storage
MEMORY USAGE user:1  # Less memory

Using Multiple Strings (worse):

SET user:1:name "Alice"
SET user:1:age 25
SET user:1:email "alice@example.com"

# Three keys, more overhead
MEMORY USAGE user:1:name + MEMORY USAGE user:1:age + ...

Hashes use less memory and fewer operations.

Common Patterns

User Profile Retrieval

# Store full profile in one hash
HSET profile:user:1 \
  name "Alice" \
  email "alice@example.com" \
  avatar "url" \
  bio "bio"

# Get profile (one operation)
profile = HGETALL profile:user:1

# Update field (one operation)
HSET profile:user:1 bio "New bio"

Shopping Cart

# Store items in user's cart
HSET cart:user:1 \
  product:101 1 \     # quantity of product:101
  product:202 3 \     # quantity of product:202
  product:303 2

# Get cart
HGETALL cart:user:1

# Add item or update quantity
HINCRBY cart:user:1 product:101 2  # Add 2 more

# Remove item
HDEL cart:user:1 product:303

Node.js (ioredis):

// Add item to cart
async function addToCart(userId, productId, quantity = 1) {
  await redis.hincrby(`cart:user:${userId}`, `product:${productId}`, quantity);
  await redis.expire(`cart:user:${userId}`, 86400 * 7); // Cart expires in 7 days
}

// Get cart contents
async function getCart(userId) {
  const cart = await redis.hgetall(`cart:user:${userId}`);
  
  return Object.entries(cart).map(([key, quantity]) => ({
    productId: key.replace('product:', ''),
    quantity: parseInt(quantity)
  }));
}

// Update item quantity
async function updateCartItem(userId, productId, quantity) {
  if (quantity <= 0) {
    await redis.hdel(`cart:user:${userId}`, `product:${productId}`);
  } else {
    await redis.hset(`cart:user:${userId}`, `product:${productId}`, quantity);
  }
}

// Remove item from cart
async function removeFromCart(userId, productId) {
  await redis.hdel(`cart:user:${userId}`, `product:${productId}`);
}

// Clear cart
async function clearCart(userId) {
  await redis.del(`cart:user:${userId}`);
}

Python (redis-py):

def add_to_cart(user_id: int, product_id: int, quantity: int = 1):
    r.hincrby(f"cart:user:{user_id}", f"product:{product_id}", quantity)
    r.expire(f"cart:user:{user_id}", 86400 * 7)  # 7 days

def get_cart(user_id: int) -> list:
    cart = r.hgetall(f"cart:user:{user_id}")
    
    return [
        {'product_id': key.replace('product:', ''), 'quantity': int(qty)}
        for key, qty in cart.items()
    ]

def update_cart_item(user_id: int, product_id: int, quantity: int):
    if quantity <= 0:
        r.hdel(f"cart:user:{user_id}", f"product:{product_id}")
    else:
        r.hset(f"cart:user:{user_id}", f"product:{product_id}", quantity)

def remove_from_cart(user_id: int, product_id: int):
    r.hdel(f"cart:user:{user_id}", f"product:{product_id}")

def clear_cart(user_id: int):
    r.delete(f"cart:user:{user_id}")

# Usage
add_to_cart(1, 101, 2)
add_to_cart(1, 202, 1)
cart = get_cart(1)
update_cart_item(1, 101, 5)
remove_from_cart(1, 202)

Caching JSON Data

# Cache API response as hash instead of JSON string
HSET api:user:response:1 \
  status "active" \
  name "Alice" \
  verified true \
  tier "premium"

# Later access specific field without parsing JSON
HGET api:user:response:1 tier

Rate Limiting by Endpoint

# Track rate limit for each endpoint per user
HSET ratelimit:user:1 \
  api:posts:create 5 \      # 5 uses
  api:messages:send 10 \    # 10 uses
  api:search:query 100

# Check if user can use endpoint
count = HGET ratelimit:user:1 "api:posts:create"
if count >= LIMIT:
  REJECT("Rate limit exceeded")

# Decrement on use
HINCRBY ratelimit:user:1 "api:posts:create" -1