Caching database queries is one of the most impactful ways to slash response times, but most people miss the fact that it’s not about reducing database load, it’s about eliminating network round trips.

Let’s see it in action. Imagine a simple web application fetching user data. Without caching, the flow looks like this:

sequenceDiagram
    participant Client
    participant WebServer
    participant Database
    Client->>WebServer: Request user data (GET /users/123)
    WebServer->>Database: SELECT * FROM users WHERE id = 123;
    Database-->>WebServer: User data
    WebServer-->>Client: User data

Each SELECT statement triggers a network hop from the web server to the database, then another hop for the data to come back. For a high-traffic app, this adds up. Now, let’s introduce a cache, say Redis, right on the web server.

sequenceDiagram
    participant Client
    participant WebServer
    participant Redis
    participant Database
    Client->>WebServer: Request user data (GET /users/123)
    WebServer->>Redis: GET users:123
    alt Cache Hit
        Redis-->>WebServer: User data
        WebServer-->>Client: User data
    else Cache Miss
        WebServer->>Database: SELECT * FROM users WHERE id = 123;
        Database-->>WebServer: User data
        WebServer->>Redis: SET users:123 <User data> EX 3600
        Redis-->>WebServer: OK
        WebServer-->>Client: User data
    end

The first request for user 123 misses the cache. The web server queries the database, gets the data, and then stores it in Redis with a Time-To-Live (TTL) of 3600 seconds (1 hour). Subsequent requests for user 123 within that hour hit Redis directly. The data is available almost instantaneously because it’s in memory on the same machine as the web server, completely bypassing the network to the database.

The core problem caching solves is latency. Databases, even fast ones, involve I/O (disk reads, CPU processing) and network overhead. By storing frequently accessed, relatively static data in a low-latency in-memory store like Redis or Memcached, you drastically reduce the time a user waits for a response. This isn’t just about making your app feel faster; it’s about reducing the resources consumed by repeated, identical database queries.

The key levers you control are:

  • Cache Key Design: How you name your cached data. A good key is unique, descriptive, and predictable. For user data, users:<user_id> is common. For a list of products, products:category:<category_slug>:page:<page_number> might work.
  • Cache Invalidation Strategy: When does cached data become stale? This is the hardest part.
    • Time-To-Live (TTL): Set an expiration time. Simple, but data can be stale until it expires.
    • Write-Through: Update the cache and the database simultaneously. Slower writes, but data is always fresh.
    • Write-Around: Write directly to the database, ignoring the cache. Data only gets into the cache on a subsequent read. Good for infrequent writes.
    • Write-Behind: Write to the cache first, then asynchronously to the database. Fastest writes, but risk of data loss if the cache fails before writing to the DB.
    • Explicit Invalidation: When data changes (e.g., a user updates their profile), you manually delete the corresponding cache key. This is often the most reliable if you can hook into your data modification logic.
  • Cache Size and Eviction Policy: How much memory your cache uses and what happens when it’s full (e.g., Least Recently Used - LRU, Least Frequently Used - LFU).

The most surprising thing about cache invalidation is how often people over-engineer it. The simplest approach, TTL, works for a vast majority of use cases. If your data doesn’t need to be perfectly up-to-the-millisecond fresh, a generous TTL (minutes or hours) dramatically simplifies your application logic. Trying to achieve perfect, instantaneous cache invalidation across many services often leads to complex distributed systems that are harder to debug than the original latency problem.

When you’re setting up your cache, ensure your application is configured to use the cache connection string or host and port. For Redis, this might be redis://localhost:6379 or redis.yourdomain.com:6379. The cache client library in your language will then use this to connect and perform operations like GET, SET, and DEL.

Once you’ve mastered basic caching, the next logical step is understanding how to cache API responses at the HTTP level using tools like Varnish or Nginx’s proxy_cache module.

Want structured learning?

Take the full Caching-strategies course →