Redis and In-Memory Caching: Patterns and Pitfalls

Matt George

Fractional CTO · Founder of Injuria.ai

•12 min read•

In-memory caching is a foundational strategy for building high-performance applications. By storing frequently accessed data in Random Access Memory (RAM), systems can bypass slower disk-based databases, drastically reducing response times.[17] Accessing data from RAM takes tens to hundreds of nanoseconds, while fetching it from a fast SSD can take tens to hundreds of microseconds, a difference of three orders of magnitude. This speed is why tools like Redis[1] have become central components in modern software architecture.

Redis, however, is more than a simple cache. It's a powerful in-memory data structure server that offers sophisticated patterns for data management, but it also introduces its own set of complexities and failure modes.[18] For a technical leader or engineer, understanding its mechanics, common patterns, and operational costs is essential for making sound architectural decisions. This requires moving beyond a surface-level view to grasp the specific tradeoffs involved in deploying and managing a distributed caching layer.

The Core of In-Memory Caching

At its heart, a cache is a high-speed data storage layer that stores a subset of data, typically transient, so that future requests for that data are served faster than is possible by accessing the data's primary storage location. The primary storage, often a relational database like PostgreSQL or a NoSQL database like MongoDB, is called the "source of truth." The cache holds a copy. The core principle is the locality of reference[2]: a program will often access the same set of data or memory locations repeatedly.

The performance gains are rooted in physics. RAM access latency is typically in the range of 10 to 100 nanoseconds. In contrast, reading from a modern NVMe SSD takes 10 to 200 microseconds, and reading from a spinning disk HDD can take 2 to 10 milliseconds. An in-memory cache like Redis eliminates the disk I/O and network latency associated with querying a separate database server for common requests, resulting in a snappier user experience and reduced load on backend systems.

The Rise and Mechanics of Redis

Redis was created by Salvatore Sanfilippo[3] (known online as "antirez") and first released in 2009. The project's origin was pragmatic: Sanfilippo was trying to improve the performance of his Italian startup's real-time web analytics service. He needed a data store that could handle a high volume of writes and provide real-time insights, a task for which traditional databases were too slow. The name Redis stands for REmote DIctionary Server, which hints at its core design as a server that provides dictionary-like access to data structures over a network.

Redis communicates with clients using a simple protocol named RESP (REdis Serialization Protocol)[4]. It's a human-readable text-based protocol that makes it easy to write clients in any language. A key architectural choice is its approach to concurrency. While I/O operations were made multi-threaded in Redis 6.0, command execution remains single-threaded. This design simplifies development by preventing race conditions on data modifications, as commands are executed atomically and sequentially. You don't need to worry about locking data when performing complex operations.

Unlike simple key-value stores that only map a string key to a string value, Redis is a true data structure server.[19] It provides first-class support for several versatile data types:

  • Strings: The most basic type, storing anything from a simple text value to a serialized JSON object or a binary blob.
  • Hashes: Maps between string fields and string values, ideal for representing objects (e.g., a user profile with fields like name, email, and id).
  • Lists: A collection of string elements sorted by insertion order. They can be used as simple queues.
  • Sets: An unordered collection of unique, non-repeating string elements.
  • Sorted Sets: Similar to Sets, but each member is associated with a score. This allows them to be sorted by score, making them perfect for leaderboards or priority queues.

This rich set of server-side data structures allows developers to offload complex data manipulations to Redis itself, reducing application-side logic and network traffic.

Common Caching Patterns

How an application interacts with a cache is defined by its caching pattern. The most prevalent is Cache-Aside, but others serve different needs.

Cache-Aside (Lazy Loading)

This is the most common caching strategy. The application logic is responsible for managing the cache.

  1. The application attempts to fetch data from Redis.
  2. If the data exists (cache hit), it's returned directly to the client.
  3. If the data does not exist (cache miss), the application queries the primary database (the source of truth).
  4. The application then stores the result in Redis with a Time-To-Live (TTL).
  5. Finally, the data is returned to the client.

The main benefit is that the cache is only populated with data that is actually requested, preventing it from being filled with unused items. The downside is the latency penalty on a cache miss, as it involves three steps: cache read, database read, and cache write.

Here is a Python example demonstrating the Cache-Aside pattern using the redis-py library:

import redis
import json

# Connect to Redis. In a real app, this would be configured.
# The decode_responses=True flag makes the client return strings, not bytes.
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)

# A mock function to simulate fetching data from a primary database (e.g., PostgreSQL)
def get_user_from_db(user_id: int) -> dict:
 print(f"Database query for user_id: {user_id}")
 # In a real system, this would be a SQL query.
 # We'll simulate it by returning a dictionary.
 if user_id == 123:
 return {"id": 123, "name": "Alice", "email": "alice@example.com"}
 return None

def get_user(user_id: int) -> dict:
 """Fetches a user, using the Cache-Aside pattern."""
 # 1. Create a unique key for this piece of data.
 cache_key = f"user:{user_id}"

 # 2. Try to get data from cache.
 cached_user_json = r.get(cache_key)

 if cached_user_json:
 # Cache Hit
 print(f"Cache hit for user_id: {user_id}")
 return json.loads(cached_user_json)

 # 3. Cache Miss
 print(f"Cache miss for user_id: {user_id}")
 
 # 4. Get data from the source of truth.
 user_data = get_user_from_db(user_id)

 if user_data:
 # 5. Store data in cache with a TTL (e.g., 5 minutes).
 # We serialize the dictionary to a JSON string.
 r.setex(cache_key, 300, json.dumps(user_data))
 
 # 6. Return the data.
 return user_data

# --- Simulation ---
# First call: cache miss, hits the database
user = get_user(123)
print(f"Received: {user}")

# Second call: cache hit, returns instantly
user = get_user(123)
print(f"Received: {user}")

Write-Through

In a write-through cache, the application writes data to the cache and the database in a single transaction. The application logic first updates the Redis cache and then immediately updates the primary database. The write operation is only considered successful after both systems confirm the write.

This pattern keeps the cache and database consistently synchronized, reducing the risk of stale data. However, it introduces write latency because every write must go to two systems. It also increases complexity, as the application must handle failures where the cache write succeeds but the database write fails.

Cache Eviction and TTL

An in-memory cache has finite space. When it fills up, Redis must decide which data to remove to make room for new items. This is called eviction.

The simplest way to manage cache lifetime is setting a Time-To-Live (TTL) on each key using commands like EXPIRE[5] or SETEX. After the TTL expires, Redis automatically removes the key.

When memory is full and a new write comes in, Redis uses a configurable eviction policy. Some common policies include:

  • allkeys-lru: Evicts the least recently used (LRU) keys from the entire keyspace.
  • volatile-lru: Evicts LRU keys only from those that have an expiration TTL set.
  • allkeys-lfu: Evicts the least frequently used (LFU) keys from the entire keyspace (available since Redis 4.0).
  • volatile-ttl: Evicts keys with the shortest remaining time to live.
  • noeviction: Returns an error on writes when memory is full.

Choosing the right eviction policy depends on your application's access patterns. allkeys-lru is a common default for general-purpose caching.

Redis Beyond Caching

It's a common misconception that Redis is only a cache. Its persistence features allow it to function as a fast, durable primary datastore for certain workloads.

Redis offers two persistence mechanisms[6]:

  1. RDB (Redis Database): RDB persistence performs point-in-time snapshots of your dataset at specified intervals. For example, you can configure it to save the dataset if at least 10 keys have changed in the last 5 minutes. Snapshots are compact and fast to load, making them ideal for backups and disaster recovery. The BGSAVE command forks the main process to save the snapshot in the background without blocking the server.

  2. AOF (Append Only File): AOF persistence logs every write operation received by the server. When Redis restarts, it replays the AOF to rebuild the state. This provides much better durability than RDB. You can configure how often the data is synced to disk (fsync), trading performance for durability. The everysec policy offers a good balance, providing durability with minimal performance impact.

With these features, Redis is often used as a primary store for session data, real-time leaderboards (using Sorted Sets), job queues (using Lists or Streams), and real-time analytics.[20][21]

Failure Modes and Operational Costs

Deploying a caching layer introduces a new moving part into your system, and with it, new potential problems.

The Thundering Herd Problem

This classic caching issue occurs when a very popular cached item expires. Suddenly, multiple concurrent application threads or processes all experience a cache miss for the same item. They all proceed to query the primary database simultaneously, creating a massive spike in load that can overwhelm the database. This is a thundering herd.

A common solution is to use a Redis lock. When a process misses the cache, it tries to acquire a lock (e.g., using the atomic SETNX command[7], which means "SET if Not eXists"). Only the process that successfully acquires the lock will query the database and repopulate the cache. Other processes wait briefly and retry fetching from the cache.

Cache Invalidation and Stale Data

Data in the cache can become stale if the source of truth is updated and the cache is not. This is the fundamental challenge of caching. As Phil Karlton[8] famously said, "There are only two hard things in Computer Science: cache invalidation and naming things."

Strategies to manage stale data include:

  • Short TTLs: A simple and effective strategy. A short TTL (e.g., 1 to 5 minutes) limits the window during which data can be stale.
  • Explicit Invalidation: When data is updated in the primary database, the application sends a command to DELETE the corresponding key from the cache. This is difficult to implement correctly, especially in complex systems where data can be modified in multiple ways.
  • Write-Through Caching: As discussed, this keeps the cache fresh at the cost of write latency.

Memory, High Availability, and Scaling

Since Redis keeps the entire dataset in RAM, memory is a primary constraint and a significant cost factor. You must configure a maxmemory limit[9] to prevent Redis from consuming all available system memory and crashing. Monitoring memory usage is a critical operational task.

A single Redis instance is a single point of failure. To build a fault-tolerant system, you need replication. Redis Sentinel[10] is a high-availability solution that monitors Redis instances, handles automatic failover from a primary to a replica if the primary becomes unresponsive, and reconfigures clients.

For datasets that won't fit in the RAM of a single server, or for workloads that require more CPU than one server can provide, you need to scale horizontally.[22] Redis Cluster provides a way to run a Redis installation where data is automatically sharded across multiple nodes. This provides both scalability and high availability, but it adds significant operational complexity compared to a single instance.

The Modern Redis Landscape

Redis continues to evolve. As of 2024, the latest major version is Redis 7. It builds upon the I/O threading model introduced in version 6, further improving performance for concurrent workloads while maintaining the simplicity of single-threaded command execution.

All major cloud providers offer managed Redis services, such as AWS ElastiCache for Redis, Google Cloud Memorystore for Redis[11], and Azure Cache for Redis[12].[17] These services handle the operational burden of setup, patching, replication, and failover, allowing teams to focus on application development.

In early 2024, the licensing for Redis changed significantly. After years of being licensed under the permissive BSD 3-Clause license[13], Redis initially changed its license to SSPL/RSALv2. This move, intended to prevent cloud providers from offering Redis as a service without contributing back, was controversial. In response, a coalition of former Redis maintainers and companies, backed by the Linux Foundation, created a fork of the last BSD-licensed version called Valkey[16] in March 2024. As of 2025, Redis 8 adopted a tri-license model that added the OSI-approved open-source AGPLv3 license. This fork aims to continue the open-source development of the project.

A Decision Lens for Leaders

When deciding whether and how to use Redis, technical leaders should weigh the performance benefits against the architectural complexity and operational costs.

Use Redis for caching when:

  • Your application has a read-heavy workload and a few "hot" data items are requested frequently.
  • Your primary database is a bottleneck, and you need to reduce its load.
  • Your application can tolerate a small degree of data staleness.
  • Reducing latency is critical for user experience or system performance.

Be cautious or consider alternatives when:

  • Your caching needs are simple and local to a single application instance. An in-process cache might be sufficient and less complex.
  • You intend to use it as a primary database without fully understanding and planning for its persistence, backup, and failure models.
  • Your dataset is significantly larger than your memory budget, and the access patterns are unpredictable (high churn), which would lead to poor cache hit ratios.
  • Your team lacks the operational experience to manage a distributed system, and a managed cloud service is not an option.

Ultimately, integrating Redis is an architectural decision that trades simplicity for performance. By understanding its core mechanics, common patterns, and potential pitfalls, you can make an informed choice that aligns with your system's requirements and your team's capabilities.

Sources (22)
  1. Redis
  2. Locality of reference
  3. Salvatore Sanfilippo
  4. RESP (REdis Serialization Protocol)
  5. EXPIRE command
  6. Redis Persistence
  7. SETNX command
  8. Two Hard Things - Martin Fowler
  9. maxmemory configuration
  10. Redis Sentinel
  11. Google Cloud Memorystore for Redis
  12. Azure Cache for Redis
  13. BSD 3-Clause License
  14. Redis Source Available License v2 (RSALv2)
  15. Server Side Public License v1 (SSPLv1)
  16. Linux Foundation Launches Valkey
  17. Dario Cabianca, Justin Roberts, AWS Certified CloudOps Engineer Study Guide (John Wiley & Sons, 2026)
  18. Jay Kreibich, Redis: The Definitive Guide (O'Reilly Media, Incorporated, 2013)
  19. Shashank Tiwari, Professional NoSQL (John Wiley & Sons, 2011)
  20. Tieme Woldman, Hands-On Microservices with Django (Packt Publishing Ltd, 2024)
  21. Brandon Rich, AWS Certified Developer Study Guide (John Wiley & Sons, 2025)
  22. Nick Alteen, Jennifer Fisher, Casey Gerena, AWS Certified Developer Official Study Guide, Associate Exam (John Wiley & Sons, 2019)

Recommended reading

How Web Caching and CDNs Work7 min readThe fastest request is the one you never have to make. Web caching is the layered system that avoids repeated work, and it hides one of the genuinely hard problems in computing. Here is how it holds together, and how it quietly leaks private data when it goes wrong.Database Replication and Read Replicas: A Guide to Consistency and Availability12 min readA comprehensive technical guide to database replication, explaining the mechanics of read replicas, the critical tradeoff between consistency and availability, and the decision framework for using them effectively.How Database Indexes Work: B-Trees and Why Your Query Is Slow12 min readDatabase indexes, typically implemented as B-Trees, accelerate data retrieval by creating a sorted, navigable structure. Understanding their mechanics, costs, and tradeoffs is crucial for diagnosing and fixing slow queries.Connection Pooling: The Silent Bottleneck in Web Applications14 min readConnection pooling is an essential performance optimization that reuses expensive network connections, but misconfigurations are a common and insidious cause of application slowdowns and outages.ACID, Isolation Levels, and the Anomalies They Prevent13 min readA comprehensive guide for engineers on the ACID properties of database transactions, with a deep focus on isolation levels, the concurrency anomalies they prevent, and the practical tradeoffs involved in choosing them.The R-Tree19 min readThe spatial index behind geographic databases, introduced by Antonin Guttman in 1984, and how bounding rectangles accelerate location queries.