Home / News / Laravel Cache Tip: Avoid Redundant has/missing Calls

Laravel Cache Tip: Avoid Redundant has/missing Calls

The Common Pattern (With Redundancy) (Sarcastic)

When working with Laravel’s cache, I often see developers using a pattern that, while functional, creates unnecessary overhead. Let me show you a simple optimization that can improve both performance and memory usage.

public function getUserCountOptimized(): int
{
    $cacheKey = 'user-count';
    $cacheTtl = Carbon::now()->
public function getUserCount(): int
{
    $cacheKey = 'user-count';
    $cacheTtl = Carbon::now()->addMinutes(10);

    // Bad: two cache ops
    if (Cache::has($cacheKey)) {
        return (int) Cache::get($cacheKey);
    }

    $userCount = User::query()->count();
    Cache::put($cacheKey, $userCount, $cacheTtl);

    return $userCount;
}

This works perfectly, but there's a subtle inefficiency: the cache is accessed twice when the key exists.
Looking at Laravel Telescope, you'll see:

Action | Key
hit    | user-count  (line 31)
hit    | user-count  (line 32)

The Better Approach: Skip has()

Instead of checking if the key exists first, we can directly attempt to retrieve it:

public function getUserCountOptimized(): int
{
    $cacheKey = 'user-count';
    $cacheTtl = Carbon::now()->addMinutes(10);

    // Good: only one cache op
    $cachedValue = Cache::get($cacheKey);

    if ($cachedValue !== null) {
        return (int) $cachedValue;
    }

    $userCount = User::query()->count();
    Cache::put($cacheKey, $userCount, $cacheTtl);

    return $userCount;
}

Or just use Laravel's remember() method, which combines the check and retrieval into a single operation:

public function getUserCountRemember(): int
{
    $cacheKey = 'user-count';
    $cacheTtl = Carbon::now()->addMinutes(10);

    return Cache::remember($cacheKey, $cacheTtl, function () {
        return User::query()->count();
    });
}

This results in only one cache hit, reducing overhead:

Action | Key
hit    | user-count  (line 31)

When remember() Isn't Enough

So, yeah - Cache::remember() is fine if you're just dealing with basic stuff. However, remember() isn't always practical. For complex scenarios, like caching third-party API responses with conditional logic, remember() may not fit. Here's a real-world example from a third-party API integration that needed extra hoops:

/**
 * Flow: from cache → try HTTP → handle response → cache it
 */
private function performRequest(string $endpoint, array $data = []): array
{
    $cacheKeyParams = ['endpoint' => $endpoint, ...$data];
    $cacheTags = [CacheKeyEnum::TEHNOMIR->tag()];

    if (isset($cacheKeyParams['code'])) {
        $cacheTags[] = CacheKeyEnum::SEARCH_ARTICLE->tag([$cacheKeyParams['code']]);
    }

    $cacheKey = CacheKeyEnum::TEHNOMIR->key($cacheKeyParams);
    $cacheTtl = TehnomirApiEndpointEnum::getCacheTtl($endpoint);

    // Return the cached result first if caching is enabled
    if ($this->cacheEnabled) {
        if (Cache::tags($cacheTags)->has($cacheKey)) {
            $responseData = Cache::tags($cacheTags)->get($cacheKey);
            if ($responseData !== null) {
                return $responseData;
            }
        }
    }

    // Make API call
    try {
        $response = $this->getClient()->post($endpoint, $data)->throw();

        $responseData = $this->responseHandler($response);

        if (!empty($responseData)) {
            Cache::tags($cacheTags)->put($cacheKey, $responseData, $cacheTtl);
        }

        return $responseData;
    } catch (Throwable $exception) {
        return $this->handleException($exception, $endpoint, $data);
    }
}

This method handles monster-sized API responses (e.g., 1MB JSON) and logs two cache hits:

  • hit: tehnomir|endpoint=pricesearch|... (line 114)
  • hit: tehnomir|endpoint=pricesearch|... (line 115)

You can optimize it by removing the has check:

if ($this->cacheEnabled) {
    $responseData = Cache::tags($cacheTags)->get($cacheKey);
    if ($responseData !== null) {
        return $responseData;
    }
}

So, How Much Faster?

I actually ran the numbers. With huge api responses:

Before (with has()):

  • About 14ms per execution
  • 47MB memory
  • 2 cache operations

After (without has()):

  • Around 6ms
  • 43MB
  • Only 1 cache op

Cache Performance: Before vs. After

Pattern Time Memory Cache Ops
With has() 14ms 47MB 2
Without 6ms 43MB 1

Both are snappy, but hey, that's almost double the speed and less memory. The optimized version shows measurable improvements in both execution time and memory usage.

What's the Point?

Avoid unnecessary Cache::has() or Cache::missing() calls when you immediately follow them with Cache::get(). Each cache operation has overhead, and eliminating redundant calls improves performance, especially when dealing with large cached values.

The pattern is simple:

  1. Try to get the value directly
  2. Check if it's null
  3. Proceed accordingly

It's a tiny change, but when multiplied across thousands of requests, the difference is real. One less cache call per request means faster responses and lower memory usage - for free.

Ever run into cache weirdness or found other sneaky optimizations in Laravel? Drop your stories below.

Tagged:

Leave a Reply

Your email address will not be published. Required fields are marked *