LW IT Solutions
« Blog Overview /Digital Marketing / Google Ads Data Retention: 37 Months of...
This post in other languages:

Google Ads Data Retention: 37 Months of Performance Data and a Resumable Backfill Job

Google Ads Data Retention: 37 Months of Performance Data and a Resumable Backfill Job
Contents
  1. 1. What the retention page actually says
  2. 2. Work out the actual deadline, then find what is already gone
  3. 3. A backfill job that resumes
  4. 4. Cron, ordering, and the loss ledger
  5. 5. Summary
  6. Sources

Since 1 June 2026, Google Ads data has an expiry date. Performance data is kept for 37 months, account change history for 11 years, Reach and Frequency data for 3 years. Today is 10 August 2026, so the oldest performance day still available is 10 July 2023, and that line moves forward by one day every day. Where the reporting history lives only inside Google Ads, a day of it disappears quietly every day. This article does the arithmetic, corrects the numbers that are being misquoted alongside it, and builds a resumable backfill job that moves everything above the line into a warehouse under local control.

1. What the retention page actually says

Three windows are documented, and only three:

  • 37 months for performance data.
  • 11 years for the history of account changes.
  • 3 years for Reach and Frequency.

Most write-ups of this change carry three further numbers along with it: 36 months for the GA Data API, 24 months for the BigQuery Data Transfer Service, 24 months for DV360 and CM360. Those values do not appear on the Google Ads help page that announced the retention windows. They are undocumented as far as this announcement goes, and passing them on as if they were part of it is how a rumour becomes a planning assumption. Plan against the three values above. A retention figure for GA4, for BigQuery transfers or for DV360 and CM360 belongs taken from that product’s own documentation.

One detail is easy to misread: 37 months is longer than three years. Reach and Frequency therefore expires about a month before the performance data covering the same campaigns. Where both are backed up, R&F has the tighter deadline, not the looser one.

2. Work out the actual deadline, then find what is already gone

The boundary is a rolling one, not a single cut that happened in June. Compute it rather than remembering it:

from datetime import date
from dateutil.relativedelta import relativedelta

today = date(2026, 8, 10)
print(today - relativedelta(months=37))  # performance      2023-07-10
print(today - relativedelta(years=3))    # reach, frequency 2023-08-10
print(today - relativedelta(years=11))   # change history   2015-08-10

Anything dated before 10 July 2023 is already unrecoverable through the API. Tomorrow that becomes 11 July 2023. Before writing any export code, it pays to establish how much of the still-available range is actually held locally. Against a Postgres warehouse with a daily table:

WITH expected AS (
  SELECT generate_series(DATE '2023-07-10', CURRENT_DATE - 1, INTERVAL '1 day')::date AS d
)
SELECT count(*) AS missing_days, min(e.d) AS oldest_gap, max(e.d) AS newest_gap
FROM expected e
LEFT JOIN ads_daily a ON a.day = e.d
WHERE a.day IS NULL;

Run the same query with the window start set to the earliest expected day. Everything it reports below 2023-07-10 is a permanent hole; everything above it is still recoverable.

A timeline with a 37-month retention window whose left edge moves daily, older data falling out into a self-hosted warehouse
Three limits, one moving edge. Everything to the left of it has already left Google Ads, so the export job is only useful while it runs ahead of the edge rather than behind it.

3. A backfill job that resumes

I am not going to print a GAQL field list here, because the right one is the one already in use. Take the resource, the date segment and the selected fields straight out of the report running today, put them in a config file, and let the job own nothing but the date window:

{
  "customer_id": "1234567890",
  "dsn": "postgresql://ads@localhost/warehouse",
  "resource":   "<the resource your existing report reads>",
  "date_field": "<the date segment that report groups by>",
  "fields":     ["<field 1>", "<field 2>"],
  "query_template": "SELECT {fields} FROM {resource} WHERE {date_field} BETWEEN '{start}' AND '{end}'",
  "oldest_wanted": "2019-01-01"
}

The runner fetches exactly one window per invocation and records it. State is four columns: customer_id, window_start, window_end, rows, written in the same transaction as the data, so a crash simply repeats the window.

import json
from datetime import date, timedelta
from dateutil.relativedelta import relativedelta
import psycopg

CFG, CHUNK = json.load(open("/etc/ads-backfill/config.json")), timedelta(days=7)
CID = CFG["customer_id"]

def next_window(cur):
    cur.execute("SELECT max(window_end) FROM backfill_state WHERE customer_id = %s", (CID,))
    (last,) = cur.fetchone()
    floor = max(date.today() - relativedelta(months=37),
                date.fromisoformat(CFG["oldest_wanted"]))
    start = last + timedelta(days=1) if last else floor
    if start < floor:                    # job stalled, those days expired
        cur.execute("INSERT INTO backfill_lost VALUES (%s, %s, %s)",
                    (CID, start, floor - timedelta(days=1)))
        start = floor
    end = min(start + CHUNK - timedelta(days=1), date.today() - timedelta(days=1))
    return (start, end) if start <= end else None

with psycopg.connect(CFG["dsn"]) as conn, conn.cursor() as cur:
    win = next_window(cur)
    if win:
        start, end = win
        rows = run_report(CID, CFG["query_template"].format(
            fields=", ".join(CFG["fields"]), resource=CFG["resource"],
            date_field=CFG["date_field"], start=start, end=end))
        cur.executemany(CFG["insert_sql"], rows)
        cur.execute("INSERT INTO backfill_state VALUES (%s, %s, %s, %s, now())",
                    (CID, start, end, len(rows)))
        conn.commit()

run_report is the existing API wrapper, whichever client library it wraps. Everything else in the file works on the local side of the line: local config, local table, local transaction boundary.

4. Cron, ordering, and the loss ledger

# /etc/cron.d/ads-backfill — one window per hour, oldest first
7 * * * * ads /opt/ads-backfill/.venv/bin/python /opt/ads-backfill/backfill.py >> /var/log/ads-backfill.log 2>&1

Two things about that job are deliberate. It walks forward from the boundary, not backward from today, because the oldest data is the only data with a deadline; recent days can wait. And when it finds that the boundary has overtaken its own progress, it does not silently skip the gap, it writes the span into backfill_lost. That table is the honest answer to “which periods do we no longer have”, and it is worth putting on a dashboard next to the gap query from section 2.

The arithmetic is comfortable. From 10 July 2023 to yesterday is 1,126 days, which is 161 windows of seven days. At one window per hour that is under seven days of wall clock, during which the boundary advances by seven days: that is 168 days of history gained for every one lost. Once the backlog is closed, the same job keeps the tail topped up and costs one API call per hour.

5. Summary

The result is a boundary that gets computed rather than remembered, a query that names which days inside the still-available window are missing from the warehouse, and a single-window runner that resumes cleanly after a crash and clamps itself to the current cutoff. On top of that come an hourly cron entry and a ledger of the periods that are already beyond recovery. The whole backlog from 10 July 2023 onward closes in about a week of unattended running, after which the job becomes maintenance.

The gain is that the reporting history stops being someone else’s retention policy. Year-over-year comparisons, seasonality models and attribution rebuilds all need data older than 37 months eventually, and after 1 June 2026 the only copy that will still exist is the one kept locally. Confirm the three documented windows in the primary source and ignore the extra figures circulating with them: Google Ads data retention policy.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

2 comments

  1. Elena Sorokina

    Thank you for separating the documented windows from the ones that travel alongside them. I had the 24-month figure in a planning document and could not remember where it came from.

    Which raises the practical question: those numbers are not invented out of nothing, and some of them may well be right for their own product. Is the objection that they are wrong, or that they are attached to the wrong announcement?

    1. Lukas Wojcik Author

      The second one, and the distinction matters more than it sounds.

      Three windows are documented on the Google Ads retention page and only three: 37 months for performance data, 11 years for account change history, 3 years for Reach and Frequency. Everything else that circulates with this change — 36 months for the GA Data API, 24 months for the BigQuery Data Transfer Service, 24 for DV360 and CM360 — is absent from that page.

      Some of those figures may be accurate for their own products. The problem is the provenance: a number carried over from a different announcement inherits an authority it was never given, and nobody rechecks it afterwards because it arrived inside a well-sourced article. Taking each retention figure from its own product’s documentation costs a few minutes and removes the whole class of error.

Write a comment

The email address is not published. Required fields are marked with an asterisk.

ALL ARTICLES & CATEGORIES

CCTV

Follow this category by RSS

Cloud & AI

Follow this category by RSS

Data Privacy

All 11 articles in this category Follow this category by RSS

Digital Analytics

All 44 articles in this category Follow this category by RSS

Digital Marketing

All 25 articles in this category Follow this category by RSS

IT & Networks

All 15 articles in this category Follow this category by RSS

Raspberry PI

Follow this category by RSS

Smart Home

All 11 articles in this category Follow this category by RSS

Web Development

Follow this category by RSS

WordPress Plugins & Tricks

Follow this category by RSS