Skip to content
CrawlerData
Menu
Guides

SEO rank data: what to store for every check, and how big it gets

A rank check returns about 6 KB. See the 293-byte record we store, the schema, the index that matters and what a year of daily checks weighs.

CrawlerData teamPublished 7 min read

Contents

Rank data is a table. Every check adds a row: which keyword, where you ranked, and a few facts about the page. How you design that row decides what you can answer six months from now: when did the AI Overview appear, which competitor moved up, did mobile and desktop diverge?

This guide covers what to store for every check, the code to build it from a search response, and what it costs in storage. The numbers come from searches we ran through the Google Search API in October 2026. Results change, so yours will differ.

What one check weighs

We ran 8 searches, on desktop and mobile, and measured the response:

Measure Result
Full response, average 6.0 KB (range 3.7 to 7.9 KB)
Full response, gzipped 2.3 KB on average
organic_results only 3.1 KB on average
Organic results per page 8.4 on average, from 6 to 11

A response is small. You can afford to keep the parts you need, and it is easy to decide how much of the rest to throw away.

The record

A rank record answers one question, “where is this domain for this keyword, in this context?”, and keeps enough about the page to explain a change later. This function builds one from a search response:

import json
import sqlite3
from datetime import datetime, timezone
from urllib.parse import urlparse


def domain(link):
    return (urlparse(link or "").hostname or "").removeprefix("www.")


def rank_record(result, domain_to_track, keyword, country="us", device="desktop", location=None):
    """One row per check: where the domain ranks, plus what else was on the page."""
    organic = result["organic_results"]
    rank = next((i for i, r in enumerate(organic, 1) if domain(r["link"]).endswith(domain_to_track)), None)
    aio = result["ai_overview"]
    return {
        "checked_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
        "keyword": keyword,
        "country": country,
        "device": device,
        "location": location,
        "rank": rank,
        "url": organic[rank - 1]["link"] if rank else None,
        "results": len(organic),
        "top10": [domain(r["link"]) for r in organic[:10]],
        "ai_overview": aio is not None,
        "cited": bool(aio and any(domain(s["link"]).endswith(domain_to_track) for s in aio["sources"])),
        "paa": len(result["people_also_ask"]),
        "local_pack": len(result["local_pack"]),
        "knowledge_panel": result["knowledge_panel"] is not None,
        "top_stories": len(result["top_stories"]),
    }

For what is inflation on desktop, tracking wikipedia.org, it returned:

{
  "checked_at": "2026-10-17T04:40:11+00:00",
  "keyword": "what is inflation",
  "country": "us",
  "device": "desktop",
  "location": null,
  "rank": 6,
  "url": "https://en.wikipedia.org/wiki/Inflation",
  "results": 7,
  "top10": ["federalreserve.gov", "imf.org", "clevelandfed.org", "equifax.com", "fidelity.com", "en.wikipedia.org", "clevelandfed.org"],
  "ai_overview": true,
  "cited": true,
  "paa": 4,
  "local_pack": 0,
  "knowledge_panel": false,
  "top_stories": 0
}

Each field is there for a reason:

  • keyword, country, device and location are the context. A rank means nothing without them, as shown in mobile vs desktop, results by country and results by city. Store location as null when you do not set one.
  • rank and url. rank is your position among organic results, and None means not found. url records which of your pages ranked, so you notice when it changes.
  • results is the number of organic results on the page. It matters, because pages do not hold 10. Our 8 searches returned between 6 and 11, so a rank of 7 on a page of 7 is last place, and on a page of 11 it is not.
  • top10 keeps the competitors, as domains. This is what lets you answer “who took our place?” later without refetching anything.
  • ai_overview and cited. Whether an overview showed, and whether it cited you. See AI Overview tracking for why the rate over many checks matters.
  • paa, local_pack, knowledge_panel, top_stories are counts or flags for the other features. They explain rank changes: a rank can fall because a block moved above you, not because you lost ground.

The eight searches

Here is the record for each of the 8 searches, tracking reddit.com for seven of them and wikipedia.org for what is inflation:

Search Device Rank Results on page AI Overview Cited
best running shoes desktop 3 9 yes no
how to learn python desktop 2 7 no no
project management software mobile 2 11 yes no
what is inflation desktop 6 7 yes yes
best vpn mobile 1 10 yes yes
how to start a podcast desktop 2 8 yes no
standing desk desktop none 6 yes no
best credit card mobile none 9 yes no

The page size varied from 6 to 11, two searches did not have the tracked domain at all, and seven of the eight had an AI Overview. Those are the details a bare rank number throws away.

The table and the index

SCHEMA = """CREATE TABLE IF NOT EXISTS ranks (
    checked_at TEXT, keyword TEXT, country TEXT, device TEXT, location TEXT,
    rank INTEGER, url TEXT, results INTEGER, top10 TEXT,
    ai_overview INTEGER, cited INTEGER, paa INTEGER, local_pack INTEGER,
    knowledge_panel INTEGER, top_stories INTEGER)"""


def save(db, rec):
    db.execute("INSERT INTO ranks VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (
        rec["checked_at"], rec["keyword"], rec["country"], rec["device"], rec["location"], rec["rank"], rec["url"],
        rec["results"], json.dumps(rec["top10"]), int(rec["ai_overview"]), int(rec["cited"]), rec["paa"],
        rec["local_pack"], int(rec["knowledge_panel"]), rec["top_stories"]))


db = sqlite3.connect("ranks.db")
db.execute(SCHEMA)
db.execute("CREATE INDEX IF NOT EXISTS idx_ranks ON ranks (keyword, device, country, checked_at)")

Do not skip the last line. The query most dashboards need is the latest check for each keyword:

SELECT keyword, device, rank FROM ranks r
WHERE checked_at = (SELECT MAX(checked_at) FROM ranks
                    WHERE keyword = r.keyword AND device = r.device AND country = r.country)

We timed it on a table filled with the 8 real records, repeated across different keywords and dates:

Rows Index Time
2,000 none 0.121 s
2,000 on keyword, device, country, checked_at 0.002 s
30,000 none did not finish in two minutes, so we stopped it
30,000 on keyword, device, country, checked_at 0.050 s

Without the index the query slows down much faster than the table grows, because it scans the table once per keyword. With it, 30,000 rows took 0.05 seconds. The index added only about 1.6 MB to a 10.4 MB database.

How big a year gets

We filled a table with 30,000 rows, 1,000 keywords for 30 days, using the shape of the real records, and measured the file:

What you store Per check 1,000 keywords daily for a year
Compact record in SQLite 293 bytes about 107 MB
Full response, uncompressed 6.0 KB about 2.2 GB
Full response, gzipped 2.3 KB about 0.85 GB

The records are repeated copies of 8 real ones with changed keywords and dates, so treat the figures as the size of this record shape, not a forecast for your keywords. The point is the ratio: compact records are about twenty times smaller than raw responses. A sensible split is to keep the compact record for every check forever, and keep full responses only for a short window, in case you want to look at a page again.

Putting it together

Fetch with the function from the rank tracking guide, then record:

result = google_search("what is inflation", country="us", device="desktop")
save(db, rank_record(result, "wikipedia.org", "what is inflation"))
db.commit()

From there, local rank tracking adds the business view, and running thousands of searches shows how to run it at scale.

What it costs

Every check is 1 credit, on every queue and device. Starter is $1.00 per 1,000 requests and the largest pack is $0.30 per 1,000, see pricing.

What you track Checks a month At $1.00 per 1,000 At $0.30 per 1,000
100 keywords, daily, desktop 3,000 $3.00 $0.90
1,000 keywords, daily, desktop 30,000 $30.00 $9.00
1,000 keywords, daily, desktop and mobile 60,000 $60.00 $18.00

Failed requests are not charged.

Next steps

You now know what one check weighs, which fields to keep, how to index the table and what a year of daily checks takes on disk. The Google Search API page lists every parameter with a full example response, and SEO rank tracking shows how teams use it.

Frequently asked questions

What should I store for each rank check?
The keyword, country, device and location, the rank and the ranking URL, how many organic results there were, the top domains, and a few flags for what else was on the page: AI Overview, whether it cited you, People Also Ask, local pack, knowledge panel and top stories. The rank_record function in this guide builds that from one search response.
How big is a Google search response?
In 8 searches we ran in October 2026, a full response was 6.0 KB on average, from 3.7 to 7.9 KB, and 2.3 KB gzipped. The organic_results part alone averaged 3.1 KB, with 8.4 results per page.
How much storage does rank tracking need?
As compact records, about 293 bytes per check in SQLite, which is roughly 107 MB for 1,000 keywords checked daily for a year. Storing every full response would be about 2.2 GB for the same year, or 0.85 GB gzipped.
Do I need a database index?
Yes. On a test table of 30,000 rows, the query that finds the latest check for every keyword did not finish within two minutes without an index. With one on keyword, device, country and checked_at it took 0.05 seconds. The index added about 1.6 MB.
Why store the number of results?
Because a page does not always have 10. Our 8 searches returned between 6 and 11 organic results, so a rank of 7 means something different on a page of 7 than on a page of 11.
How much does it cost to collect rank data?
1 credit per check. 1,000 keywords checked daily is 30,000 credits a month: $30 at the Starter rate of $1.00 per 1,000 requests, and $9 at the largest pack's $0.30 per 1,000.
  • SEO rank data
  • Rank tracking
  • SEO ranking API
  • SQLite
  • Data modeling
  • Python