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 Case | Cache Duration | Reason |
|---|---|---|
| Rank tracking (daily) | 24 hours | Check once per day is sufficient |
| Competitor monitoring | 12 hours | Detect same-day movements |
| Brand monitoring | 1 hour | Catch breaking news quickly |
| AI agent grounding | 0 (live) | Freshness critical for accuracy |
| Content research | 7 days | Data changes slowly |
| Historical analysis | Forever | Never re-fetch old data |
| Price monitoring | 6 hours | Prices 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
| Strategy | Monthly Searches | Cost (Cached) | Cost (No Cache) | Savings |
|---|---|---|---|---|
| Daily rank tracking | 1,000 | $0.50 | $0.50 | 0% |
| Hourly monitoring | 30,000 | $3.00 | $15.00 | 80% |
| Content research | 5,000 | $0.50 | $2.50 | 80% |
| Multi-market (200 keywords, 5 markets, daily) | 30,000 | $15.00 | $15.00 | 0% |
| Multi-market (200 keywords, 5 markets, hourly) | 720,000 | $15.00 | $360.00 | 96% |
Cache TTL Recommendations by Use Case
| Scenario | TTL | Rationale |
|---|---|---|
| Blog post rankings | 24h | Positions rarely change intraday |
| News results | 15min | News freshness matters |
| Local Pack | 6h | Local results change slowly |
| Shopping prices | 1h | Prices change frequently |
| Featured snippets | 1h | Snippets rotate often |
| AI Overviews | 30min | AI summaries change rapidly |
| PAA questions | 24h | PAA sets are relatively stable |
| Knowledge Graph | 7 days | Entity 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
- Cache by query + geo — same keyword in different markets needs separate caches
- Set appropriate TTLs per use case — not all data is equally time-sensitive
- Implement stale-while-revalidate — serve cached data, refresh in background
- Monitor cache hit rates — low hit rate means TTL is too short (or queries are too unique)
- 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.