WriteMySEO / Blog / Building an SEO Data Stack That Outlives the UI
SEO data

Building an SEO Data Stack That Outlives the UI

Search Console keeps 16 months and caps exports at 1,000 rows. A warehouse removes both limits and makes the analyses that matter possible for the first time.

Every serious SEO analysis eventually hits the same two walls: the 16-month retention window and the 1,000-row UI export cap. Both are removable, and doing so changes what questions you can ask.

What you are building

Three layers, none of them exotic:

Extraction. Get data out of Search Console, analytics, and your crawler on a schedule. Storage. Put it somewhere with no retention limit and a query language. Analysis. SQL, joined across sources.

For most sites this is a weekend of setup and near-zero ongoing cost.

The easy path: the BigQuery bulk export

Search Console can write directly to BigQuery on a daily schedule. Settings → Bulk data export → configure a project and dataset. Google writes daily partitions to three tables — one for URL-level impressions, one for site-level, one for the export log.

This is the single highest-value thing in this article. It requires no code, it starts accumulating history immediately, and it captures data at a granularity the UI never exposes. Costs are typically a few dollars a month for storage on a mid-sized site.

The critical property: it only captures data from the day you enable it forward. There is no backfill. Anyone who expects to care about SEO history should enable it now rather than at the point of needing it, because the alternative is waiting a year for the history to exist.

The API path, for what the export misses

The bulk export does not cover everything — notably it has its own aggregation behavior, and you may want the query dimension shaped differently. The Search Console API fills gaps and lets you pull historical data back to the retention limit.

from google.oauth2 import service_account
from googleapiclient.discovery import build

creds = service_account.Credentials.from_service_account_file(
    "service-account.json",
    scopes=["https://www.googleapis.com/auth/webmasters.readonly"],
)
gsc = build("searchconsole", "v1", credentials=creds)

def query(site, start, end, dimensions, start_row=0):
    return gsc.searchanalytics().query(
        siteUrl=site,
        body={
            "startDate": start,
            "endDate": end,
            "dimensions": dimensions,
            "rowLimit": 25000,
            "startRow": start_row,
        },
    ).execute()

Two things to get right. Paginate — loop startRow in increments of 25,000 until a response returns no rows, or you will silently truncate. And query day by day rather than over a range, because Search Console aggregates across the requested window and you lose daily granularity you cannot recover later.

What to add alongside

Analytics data for sessions and conversions by landing page, joined to Search Console on URL. This is what lets you say "these queries produce revenue" rather than "these queries produce clicks."

Crawl data — a scheduled crawl written to the same warehouse, with status codes, titles, meta descriptions, word counts, canonical tags, and internal link counts per URL. Joining crawl data to performance data is where the highest-value analyses live.

Rank tracking, if you use it, for the query set you care about.

The analyses this unlocks

These are the ones that justify the setup, because none of them are possible in the UI:

Multi-year seasonality. Three or four years of daily data by query cluster shows you when demand actually rises in your category, which is when content needs to be live rather than when it needs to be commissioned.

Striking-distance at full depth. Every query in positions 5–15 with meaningful impressions, across the entire tail rather than the top 1,000 rows. On a large site this list is where most of the available upside sits.

Cannibalization detection. Group by query, count distinct URLs receiving impressions for it, and flag queries where the ranking URL changes frequently. That instability is the signature of two pages competing.

Content decay at scale. Year-over-year comparison for every URL at once, ranked by absolute clicks lost.

Internal links versus performance. Join crawl data to performance data, and find pages with strong impressions, weak position, and few internal inbound links. This query produces a prioritized fix list more reliably than any audit tool.

Real forecasting. Actual CTR by position from your own data, applied to your own impression trends, instead of borrowed curves.

The scale threshold

Below a few hundred URLs and a few thousand monthly clicks, the UI is genuinely adequate and this is over-engineering. The exception is retention — even a small site benefits from turning on the BigQuery export purely so the history exists later.

Above that, the constraint stops being tooling and starts being questions. Most teams find the first month of having the data produces a backlog of analyses they had not been able to ask about, which is the real return: not better dashboards, but a shift from reporting what happened to finding what to do next.

BigQueryGSC APIdata stack

WriteMySEO produces marketing content, not legal, medical, financial, or compliance advice. Figures cited reflect publicly reported industry data at time of writing and shift over time.

Get started

We write this well about your industry, every month.

AI-drafted, human-reviewed SEO content on a flat subscription. Blog posts, metadata, schema, and internal links, shipped on a monthly rhythm.

See plans

More from the blog