Back to Blog

SERP Cache Strategy: When SEO Tools Should Reuse Google Results

When to cache Google SERP results and when fresh data is required. Balance cost, accuracy, and monitoring frequency with a practical SERP cache strategy.

June 13, 2026
By SerpBase Teamserp cachingseo toolsapi cost controlgoogle resultscache strategy

Why SERP Caching Matters

Every SERP API call costs money. For SEO tools and monitoring systems, the difference between a well-cached system and an uncached one can be 10x in monthly cost.

But caching too aggressively means missing ranking changes. The right strategy balances cost against data freshness requirements.

Cache Decision Matrix

Use CaseCache DurationReason
Rank tracking (daily)24 hoursCheck once per day is sufficient
Competitor monitoring12 hoursDetect same-day movements
Brand monitoring1 hourCatch breaking news quickly
AI agent grounding0 (live)Freshness critical for accuracy
Content research7 daysData changes slowly
Historical analysisForeverNever re-fetch old data
Price monitoring6 hoursPrices change intraday

Implementing a Cache Layer

Step 1: Simple Cache Decorator

import time
import json
import hashlib

class SERPCache:
    def __init__(self, ttl_seconds=3600):
        self.cache = {}
        self.ttl = ttl_seconds

    def _make_key(self, payload):
        raw = json.dumps(payload, sort_keys=True)
        return hashlib.md5(raw.encode()).hexdigest()

    def get(self, payload):
        key = self._make_key(payload)
        if key in self.cache:
            entry = self.cache[key]
            if time.time() - entry["timestamp"] < self.ttl:
                return entry["data"]
        return None

    def set(self, payload, data):
        key = self._make_key(payload)
        self.cache[key] = {
            "data": data,
            "timestamp": time.time()
        }

Step 2: Cached API Client

import requests

class CachedSERPClient:
    def __init__(self, api_key, cache_ttl=None):
        self.api_key = api_key
        self.url = "https://api.serpbase.dev/google/search"
        self.headers = {
            "X-API-Key": api_key,
            "Content-Type": "application/json"
        }
        # Default TTLs by query type
        self.ttls = cache_ttl or {
            "rank_tracking": 86400,     # 24 hours
            "competitor": 43200,        # 12 hours
            "research": 604800,         # 7 days
            "live": 0                   # no cache
        }
        self.caches = {
            name: SERPCache(ttl)
            for name, ttl in self.ttls.items()
        }

    def search(self, query, gl="us", cache_type="rank_tracking"):
        payload = {"q": query, "gl": gl}
        cache = self.caches.get(cache_type)

        if cache and cache_type != "live":
            cached = cache.get(payload)
            if cached:
                return cached, True  # cached=True

        resp = requests.post(self.url, json=payload, headers=self.headers)
        data = resp.json()

        if cache:
            cache.set(payload, data)

        return data, False  # fresh

Step 3: Tiered Caching

class TieredCache:
    """In-memory cache with Redis fallback"""
    def __init__(self, redis_client=None):
        self.memory = SERPCache(ttl=300)  # 5 min memory
        self.redis = redis_client

    def get(self, key, query_type):
        # Check memory first
        cached = self.memory.get(key)
        if cached:
            return cached, "memory"

        # Check Redis
        if self.redis:
            cached = self.redis.get(key)
            if cached:
                data = json.loads(cached)
                self.memory.set(key, data)  # Warm memory cache
                return data, "redis"

        return None, None

    def set(self, key, data, query_type, ttl=3600):
        self.memory.set(key, data)
        if self.redis:
            self.redis.setex(key, ttl, json.dumps(data))

Cache Invalidation Strategy

def should_invalidate(cached_entry, fresh_entry):
    """Check if cached data is stale enough to warrant a refresh"""
    if not cached_entry:
        return True

    cached_ranks = {
        r["link"]: r["position"]
        for r in cached_entry.get("organic", [])
    }
    fresh_ranks = {
        r["link"]: r["position"]
        for r in fresh_entry.get("organic", [])
    }

    changes = 0
    for url, pos in fresh_ranks.items():
        if url in cached_ranks and cached_ranks[url] != pos:
            changes += 1

    # If more than 20% of results changed, refresh
    total_results = max(len(cached_ranks), 1)
    return (changes / total_results) > 0.2

Cache-Aware Rank Tracking

class SmartRankTracker:
    def __init__(self, client):
        self.client = client
        self.keyword_metadata = {}

    def check_rank(self, keyword, domain, gl="us"):
        data, cached = self.client.search(keyword, gl)

        if cached:
            age = self._get_cache_age(keyword, gl)
            if age < 60:  # minutes
                pass  # fresh enough

        for r in data.get("organic", []):
            if domain in r.get("link", ""):
                return r["position"], cached

        return None, cached

    def _get_cache_age(self, keyword, gl):
        # Track when this keyword was last checked
        key = f"{keyword}|{gl}"
        if key in self.keyword_metadata:
            return (time.time() - self.keyword_metadata[key]) / 60
        return float('inf')

Cost Savings Analysis

StrategyMonthly SearchesCost (Cached)Cost (No Cache)Savings
Daily rank tracking1,000$0.50$0.500%
Hourly monitoring30,000$3.00$15.0080%
Content research5,000$0.50$2.5080%
Multi-market (200 keywords, 5 markets, daily)30,000$15.00$15.000%
Multi-market (200 keywords, 5 markets, hourly)720,000$15.00$360.0096%

Cache TTL Recommendations by Use Case

ScenarioTTLRationale
Blog post rankings24hPositions rarely change intraday
News results15minNews freshness matters
Local Pack6hLocal results change slowly
Shopping prices1hPrices change frequently
Featured snippets1hSnippets rotate often
AI Overviews30minAI summaries change rapidly
PAA questions24hPAA sets are relatively stable
Knowledge Graph7 daysEntity data is very stable

Implementation: Express.js Cache Middleware

const cache = new Map();

function cacheMiddleware(ttlSeconds = 3600) {
  return (req, res, next) => {
    const key = JSON.stringify(req.body);
    const cached = cache.get(key);

    if (cached && Date.now() - cached.timestamp < ttlSeconds * 1000) {
      return res.json(cached.data);
    }

    // Store original send
    const originalSend = res.json.bind(res);
    res.json = (data) => {
      cache.set(key, { data, timestamp: Date.now() });
      originalSend(data);
    };

    next();
  };
}

Best Practices

  1. Cache by query + geo — same keyword in different markets needs separate caches
  2. Set appropriate TTLs per use case — not all data is equally time-sensitive
  3. Implement stale-while-revalidate — serve cached data, refresh in background
  4. Monitor cache hit rates — low hit rate means TTL is too short (or queries are too unique)
  5. Cache entire response — store the full JSON, extract fields on read

A smart cache strategy can reduce SERP API costs by 80-96% without sacrificing data quality. The key is matching TTL to your actual freshness requirements.