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,deviceandlocationare the context. A rank means nothing without them, as shown in mobile vs desktop, results by country and results by city. Storelocationas null when you do not set one.rankandurl.rankis your position among organic results, andNonemeans not found.urlrecords which of your pages ranked, so you notice when it changes.resultsis 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.top10keeps the competitors, as domains. This is what lets you answer “who took our place?” later without refetching anything.ai_overviewandcited. 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_storiesare 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