0

Cache hit vs cache miss — found in cache vs had to compute it

Beginner5 min read·eng-20-003
interviewperformance

Concept

Cache hit vs cache miss — the two outcomes when looking up data in a cache.

Cache hit: The requested data IS in the cache. The cache serves it directly — fast, no expensive computation.

Cache miss: The requested data is NOT in the cache (expired, never cached, or evicted). The system falls through to the original source (database, API, computation), gets the data, (optionally stores it in cache), and returns it — slower.

Hit rate: hits / (hits + misses). A cache with a 95% hit rate means 95% of requests are served from cache. Higher is better. If hit rate is 20%, the cache is barely helping.

Cold cache: When the cache starts empty (after a server restart, after cache:clear). All requests are misses until the cache warms up. "Cache warming" = pre-populating the cache before traffic hits.

Cache stampede (thundering herd): When a popular item expires, MANY simultaneous requests all miss the cache at the same time, all hit the database simultaneously, all computing the same expensive query. The cache fills from the first result but the stampede already happened.

Preventing cache stampede:

  • Locking: Only one process recomputes; others wait. Cache::lock() in Laravel.
  • Stale-while-revalidate: Serve stale data while refreshing in background.
  • Randomized TTLs: Instead of all items expiring at the same time, add jitter.

Monitoring cache performance: Track hit/miss rates in your monitoring (Redis's INFO stats command shows keyspace_hits and keyspace_misses).

Code Example

php
<?php
// OBSERVING HIT vs MISS
public function getProduct(int $id): Product
{
    $key = "product:{$id}";

    if (Cache::has($key)) {
        // CACHE HIT
        Log::debug("Cache hit for {$key}");
        return Cache::get($key);
    }

    // CACHE MISS
    Log::debug("Cache miss for {$key} — fetching from DB");
    $product = Product::with('variants', 'images')->findOrFail($id);
    Cache::put($key, $product, 3600);
    return $product;
}

// More idiomatic (same logic, shorter):
public function getProduct(int $id): Product
{
    return Cache::remember("product:{$id}", 3600, function () use ($id) {
        return Product::with('variants', 'images')->findOrFail($id);
        // This closure only runs on CACHE MISS
    });
}

// CACHE STAMPEDE PREVENTION — atomic lock
public function getExpensiveReport(): array
{
    $key  = 'monthly-revenue-report';
    $lock = Cache::lock('lock:' . $key, 10); // 10-second lock

    // Try to serve from cache first (fast path)
    if ($cached = Cache::get($key)) {
        return $cached;
    }

    // MISS — acquire lock so only ONE process recomputes
    if ($lock->get()) {
        try {
            $report = $this->computeMonthlyRevenue(); // expensive
            Cache::put($key, $report, 3600);
            return $report;
        } finally {
            $lock->release();
        }
    }

    // Another process holds the lock — wait and then get from cache
    $lock->block(5); // wait up to 5 seconds for lock
    return Cache::get($key) ?? $this->computeMonthlyRevenue();
}

// CACHE WARMING — fill cache before traffic hits
// Artisan command or scheduled task:
php artisan cache:warm

class WarmCacheCommand extends Command
{
    protected $signature = 'cache:warm';

    public function handle(): void
    {
        Product::each(function (Product $product) {
            Cache::remember("product:{$product->id}", 3600, fn() => $product->load('variants'));
            $this->line("Warmed product:{$product->id}");
        });
    }
}