Back to Blog

SERP Cache Strategy: When SEO Tools Should Reuse Google Results

A practical guide to caching Google SERP API responses without corrupting rank tracking, SEO audits, competitor monitoring, or content research workflows.

April 30, 2026
By SerpBase Teamserp cache strategyseo data pipelinesapi cost controlgoogle results

SERP Cache Strategy: When SEO Tools Should Reuse Google Results

The easiest way to waste search credits is not a bug in the API call. It is usually a boring product decision: every screen, worker, retry, and export asks Google for the same result again.

I have seen this show up in rank trackers, agency dashboards, keyword research tools, and AI content workflows. A user opens a report. A background job refreshes the same keyword. A retry queue runs ten minutes later. Someone exports a CSV. Four requests later, the product has learned nothing new.

Caching SERP data is not about hiding stale data from users. It is about deciding which searches actually need to be fresh.

Cache the question, not just the URL

A Google result is only reusable when the search context is identical enough. For SerpBase, the cache key should include every field that changes the result set.

At minimum, include:

  • q: the exact query after trimming obvious whitespace
  • gl: country targeting
  • hl: language targeting
  • page: page number
  • search type, if your app also calls images, news, or videos

Do not merge US and UK results. Do not merge English and Spanish results. Do not merge page one and page two. Those shortcuts look cheap until a customer asks why a report says one thing and a manual check says another.

A simple cache key can be enough:

import hashlib
import json


def serp_cache_key(payload: dict) -> str:
    normalized = {
        "q": " ".join(str(payload["q"]).split()),
        "gl": str(payload.get("gl") or "us").lower(),
        "hl": str(payload.get("hl") or "en").lower(),
        "page": int(payload.get("page") or 1),
        "type": str(payload.get("type") or "search"),
    }
    raw = json.dumps(normalized, sort_keys=True, separators=(",", ":"))
    return "serp:" + hashlib.sha256(raw.encode("utf-8")).hexdigest()

That key is intentionally plain. The hard part is not hashing. The hard part is choosing a refresh policy that matches the job.

Put keywords into freshness buckets

Not every keyword deserves the same TTL. A query about breaking news and a query about how to generate a sitemap do not move at the same speed.

A useful first pass is four buckets.

BucketExampleSuggested TTL
Volatilenews, launches, outages, brand crisis terms5-30 minutes
Commercialvendor comparisons, pricing pages, category terms6-24 hours
Evergreentutorials, glossary pages, stable how-to queries2-7 days
Internal QAdeployment checks, smoke tests, one-off auditsno reuse, or manual

The exact numbers depend on the product. A daily rank tracker can reuse results for the rest of the day. A news monitor cannot. A content brief generator may only need fresh SERPs when the user creates the brief; after that, the evidence packet can be stored with the brief forever.

The mistake is treating all SERPs as if they were equally urgent.

Store both the raw response and the snapshot you need

For most SEO tools, the raw API response is useful for debugging, but the product usually needs a smaller shape:

  • top organic URLs
  • titles and snippets
  • visible SERP features
  • People Also Ask questions
  • related searches
  • collection time

Store the normalized snapshot in your application tables. Keep the raw JSON in cheaper storage with a shorter retention window.

from datetime import datetime, timezone


def normalize_serp(query: str, gl: str, hl: str, data: dict) -> dict:
    return {
        "query": query,
        "gl": gl,
        "hl": hl,
        "collected_at": datetime.now(timezone.utc).isoformat(),
        "organic": [
            {
                "position": item.get("position"),
                "title": item.get("title"),
                "link": item.get("link"),
                "snippet": item.get("snippet", ""),
            }
            for item in data.get("organic", [])[:10]
        ],
        "features": {
            "people_also_ask": bool(data.get("people_also_ask")),
            "top_stories": bool(data.get("top_stories")),
            "knowledge_graph": bool(data.get("knowledge_graph")),
            "answer_box": bool(data.get("answer_box")),
            "local_results": bool(data.get("local_results")),
        },
    }

This keeps reporting fast and makes old data easier to compare. You should not need to parse a large raw payload every time a user loads a chart.

Fetch through SerpBase only when the cache misses

The cache wrapper should be boring. Check the key, validate freshness, call the API only when needed, then store the result with the collection time.

import requests
import time

SERPBASE_URL = "https://api.serpbase.dev/google/search"


def search_with_cache(redis, api_key: str, payload: dict, ttl_seconds: int) -> dict:
    key = serp_cache_key(payload)
    cached = redis.get(key)
    if cached:
        return json.loads(cached)

    response = requests.post(
        SERPBASE_URL,
        headers={
            "X-API-Key": api_key,
            "Content-Type": "application/json",
        },
        json={
            "q": payload["q"],
            "gl": payload.get("gl", "us"),
            "hl": payload.get("hl", "en"),
            "page": payload.get("page", 1),
        },
        timeout=30,
    )
    response.raise_for_status()
    data = response.json()
    if data.get("status") != 0:
        raise RuntimeError(data.get("error") or "search failed")

    stored = {
        "cached_at": int(time.time()),
        "payload": payload,
        "data": data,
    }
    redis.setex(key, ttl_seconds, json.dumps(stored))
    return stored

The function is small, but it prevents a common product leak: multiple callers paying for the same result because nobody owns freshness.

Force refresh when the user is making a decision

A cache should never surprise the person using the product. Give users a clear refresh button on screens where freshness matters.

Force refresh for:

  • reports being sent to clients
  • alerts that will trigger a workflow
  • pre-publish content checks
  • post-migration SEO audits
  • brand or incident monitoring
  • any query where the cached result is older than the user expects

For normal dashboard browsing, cached results are usually fine. For a client-facing export, spend the request.

That distinction is where the savings come from.

Do not cache away volatility signals

Some changes are the point of the product. If your tool monitors SERP features, freshness rules must protect volatility.

A keyword that suddenly gains Top Stories should probably bypass a long evergreen TTL. A query that starts showing a local pack may need more frequent checks in that market. A brand query during a launch, outage, or PR spike should not sit behind a two-day cache.

A practical rule is to shorten the next TTL when the current SERP contains volatile features:

def ttl_for_snapshot(snapshot: dict, default_ttl: int) -> int:
    features = snapshot.get("features", {})
    if features.get("top_stories"):
        return min(default_ttl, 30 * 60)
    if features.get("local_results"):
        return min(default_ttl, 6 * 60 * 60)
    return default_ttl

This is not perfect, but it is better than pretending every query behaves the same.

The cost math is usually obvious

Say you track 5,000 keywords across two markets. A naive dashboard might refresh every time someone opens a project.

A better setup:

  • scheduled collection runs once per day
  • dashboard reads stored snapshots
  • manual refresh is available for important keywords
  • volatile buckets refresh more often
  • exports reuse the latest snapshot unless the user asks for fresh data

If that cuts duplicate requests by 30-50%, the product gets cheaper without becoming less useful. With SerpBase pricing starting at low prepaid volumes, that can be the difference between a feature that feels expensive and one you can leave on by default.

A good cache policy is part of the product

Users do not care whether the data came from Redis, Postgres, or a fresh API call. They care whether the report is honest.

So show the collection time. Label stale snapshots. Let users refresh when they need to. Keep the cache key strict enough that markets do not bleed into each other. Shorten TTLs when the SERP itself looks volatile.

That is the whole strategy: reuse Google results when reuse preserves the decision, and call the API again when freshness changes the decision.

SerpBase gives you structured Google results through a simple API. A cache layer makes those results cheaper to use at scale without turning your SEO data into guesswork.