Search Console keeps sixteen months of performance data. Then every day, the oldest day disappears. This guide sets up the export that keeps every day from now on, and shows how to pull the sixteen months you still have before they fall out of the window.
This is the written version of my Search Console warehouse video. Every number below comes from my own export or from Google's own docs, listed at the bottom with the date I checked them.
Google keeps sixteen months of performance data. Today the window starts about sixteen months back, and every day the oldest day drops off. Not hidden, not archived. Gone, at least from your side.
Here is why that hurts. In February 2025, one of my shops had its best month ever in Search Console: 1,392 clicks. In February 2026, the same month brought 85. The page that earned 917 of those clicks fell from position two to fifteen in February 2026 and never came back. 917 clicks became 2.
In the current Search Console window, that page never shows more than about 250 clicks in a month. The months where it did nine hundred are outside the window. You would never know what you lost, which means you also can't ask what changed.
A change in November only makes sense next to last November. And the November before that. Sixteen months gives you one November. That is the whole argument for this setup.
Google's export only starts the day you switch it on. There is no backfill from Google. Their help page says it plainly: for data before your setup, use the Search Console API.
So the order is fixed: turn the bulk export on today, then pull your history through the API into the same place. Every day you wait is a day you can't get back.
Two places, five steps. The Cloud part takes about ten minutes, most of it waiting for billing to activate.
Any project works. Billing needs to be on: there is a free usage level, and storage and queries above that free level are charged. Google says this itself on the setup page, and it's why we set an expiration later in this guide.
In the Cloud Console, go to APIs & Services and enable the BigQuery API and the BigQuery Storage API on that project.
Under IAM & Admin, click Grant access and paste this service account:
search-console-data-export@system.gserviceaccount.comGive it exactly two roles: BigQuery Job User and BigQuery Data Editor. Nothing more. This is the account Search Console uses to write your data, and Google documents these two roles as the requirement.
In Search Console, go to Settings, then Bulk data export, for the property you want. Paste the Cloud project ID, not the project number; the ID is the readable one. Choose a dataset name (the default is searchconsole, and the name always starts with that string) and a location for the dataset. Exporting multiple properties into one project? Give each property its own dataset name. Pick the location with care, because you can't easily change it once exports have started.
Search Console runs a test export first. If it succeeds, owners get a mail and exports start within 48 hours, including data for the day you switched it on. The same page in Settings shows the status of recent exports afterwards.
Once the first tables exist, set the partition expiration (see the costs section below). And leave the table schema alone: adding a column to those tables makes the export fail.
The export lands in your dataset as three tables. Two of them sound like they contain the same thing. They don't.
| Table | Grain | What it adds |
|---|---|---|
| searchdata_site_impression | Aggregated by property | Query, country, device, search type |
| searchdata_url_impression | Aggregated by URL | The landing URL, rich result flags, Discover anonymization flag |
| ExportLog | One row per saved export | What was saved, for which date, and when. Failed exports are not logged here |
The same click shows up in both data tables, once at property grain and once at URL grain. Add the two totals together and you count every click twice. Pick the table that answers your question and stay in it.
One more trap. Search Console writes each day's data incrementally, so the tables contain rows with repeated keys: the same query, same date, split over several rows. Never read raw rows and trust what you see. Always SUM your metrics and GROUP BY the dimensions you actually want. Google's own query guidelines repeat this for a reason.
Search Console hides rare searches. Google calls them anonymized queries: queries that were not issued by more than a few dozen people over a couple of months. Privacy, fair enough. But the way that hiding works differs per route, and that difference is exactly where bad charts come from.
is_anonymized_query is true and the query field is an empty string. The URL table even keeps the landing page, so you can't see what someone typed, but you can see which page earned the click.| Report and API | Bulk export | |
|---|---|---|
| Row cap | 50,000 per day, per site, per search type | None |
| Reach | Sixteen months back | From the day you switch it on |
| Anonymized queries | In the totals, no rows | Own rows, empty query, page kept |
On my own export, this is not a rounding error. From 8 June to 17 October 2026, on my own sites, 60.6 percent of web clicks sat in rows without a search term: 19,152 of 31,606 clicks. On one small site it was 97.1 percent. Those are my numbers from one setup, not a benchmark, but they show the size of the blind spot.
Now the trend line problem. Your API-pulled months contain no anonymized rows. Your export months do. Draw one line across the boundary and it jumps, not because anything happened on your site, but because the export suddenly counts the clicks the API never gave you per row. Compare like with like. And when you load API history into BigQuery, put a source column on it so no query ever mixes the two silently.
BigQuery has a free usage level, and a small Search Console export usually stays inside it for storage. Above the free level, storage and queries are charged, and a busy site can grow fast. Three things keep it cheap:
Check what your own setup spends in the Cloud Console billing page after a month. That is your real number, not mine.
Replace your-project with your project ID and the site_url with your property: a domain property looks like sc-domain:example.com, a URL-prefix property is the full URL as it is defined in Search Console.
The daily trend for one property. Note the SUMs and the position formula: position is stored zero-based, so the average is the sum divided by impressions, plus one.
SELECT
data_date,
SUM(impressions) AS impressions,
SUM(clicks) AS clicks,
SUM(clicks) / SUM(impressions) AS ctr,
SUM(sum_top_position) / SUM(impressions) + 1.0 AS avg_position
FROM `your-project.searchconsole.searchdata_site_impression`
WHERE site_url = 'https://www.example.com/'
AND search_type = 'web'
AND data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY)
AND CURRENT_DATE()
GROUP BY data_date
ORDER BY data_date DESC;This is the query the interface can't give you: pages that earn clicks on rare queries. Those rows have an empty query but the URL table keeps the page, so long-tail pages show up here that never appear in your query report.
SELECT
url,
SUM(clicks) AS clicks,
SUM(impressions) AS impressions
FROM `your-project.searchconsole.searchdata_url_impression`
WHERE site_url = 'https://www.example.com/'
AND search_type = 'web'
AND data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY)
AND CURRENT_DATE()
GROUP BY url
ORDER BY clicks DESC
LIMIT 20;When you want the query list, filter the anonymized rows out, or the empty-string query will sit at the top of your list like a ghost keyword.
SELECT
query,
SUM(impressions) AS impressions,
SUM(clicks) AS clicks,
SUM(sum_position) / SUM(impressions) + 1.0 AS avg_position
FROM `your-project.searchconsole.searchdata_url_impression`
WHERE site_url = 'https://www.example.com/'
AND search_type = 'web'
AND is_anonymized_query = FALSE
AND data_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 28 DAY)
AND CURRENT_DATE()
GROUP BY query
ORDER BY clicks DESC
LIMIT 20;The bulk export handles tomorrow. The API handles the past, while it is still there. I'm not going to paste a backfill script I didn't run for you today, so here are the steps instead; they fit in an evening:
Use a Google Cloud account that has access to the property: gcloud auth application-default login is enough for a one-off run, or a service account you added as a user in Search Console.
Start date: today minus sixteen months. For each property and each search type you care about, call the Search Analytics endpoint with date as the dimension, plus query and page for the detail you want, one day at a time.
Ask for 25,000 rows, then use the row offset to pull the second half. That is the 50,000-per-day-per-search-type ceiling. Bigger sites need both pages; smaller ones finish on the first.
Write the rows into a table next to your export tables and add a column that says where a row came from: api or bulk export. The API months contain no anonymized rows, so any query that mixes both sources without that column will lie to you later.
Pick three random days, compare totals with the Search Console performance report, and only trust the backfill once those match. The totals should match; the row sums will not, because of the anonymized queries.
The window only shrinks. Sixteen months from your setup day, every day you did not save is gone for good.
The video that goes with this guide: a best month that Google had already pushed out of the sixteen-month window, and the three moves that keep the rest of your history.
One shop's best month ever, the page that quietly earned 917 of those clicks, and what the saved window showed a year later.
Watch on YouTube // next guideThe other half of running agents on real work: keeping your Claude weekly limit alive with one settings line and a daily count.
Read the guideThe warehouse, the daily checks and the agents that read it, installed on your own server. I send a plan that fits what you run.