37 Monate und Schluss: Google-Ads-Daten verfallen jetzt nach Fahrplan
Inhalt
Seit dem 01.06.2026 haben Google-Ads-Daten ein Verfallsdatum. Leistungsdaten werden 37 Monate aufbewahrt, der Änderungsverlauf eines Kontos 11 Jahre, Reach-and-Frequency-Daten 3 Jahre. Heute ist der 10.08.2026, der älteste Leistungstag, den Sie noch abrufen können, ist damit der 10.07.2023 – und diese Linie rückt jeden Tag um einen Tag weiter. Wenn Ihre Reporting-Historie ausschließlich in Google Ads liegt, verlieren Sie davon still und leise pro Tag einen Tag. Dieser Artikel rechnet das durch, korrigiert die Zahlen, die im selben Atemzug falsch zitiert werden, und baut einen wiederaufnehmbaren Backfill-Job, der alles oberhalb der Linie in Ihr eigenes Warehouse holt.
1. Was auf der Seite zur Datenaufbewahrung tatsächlich steht
Dokumentiert sind drei Zeiträume – und nur diese drei:
- 37 Monate für Leistungsdaten.
- 11 Jahre für den Änderungsverlauf des Kontos.
- 3 Jahre für Reach and Frequency.
In den meisten Beiträgen zu dieser Änderung tauchen drei weitere Zahlen mit auf: 36 Monate für die GA Data API, 24 Monate für den BigQuery Data Transfer Service, 24 Monate für DV360 und CM360. Diese Werte stehen nirgends auf der Google-Ads-Hilfeseite, die die Aufbewahrungsfristen angekündigt hat. Im Rahmen dieser Ankündigung sind sie schlicht undokumentiert – und wer sie weitergibt, als gehörten sie dazu, macht aus einem Gerücht eine Planungsgrundlage. Planen Sie mit den drei Werten oben. Wenn Sie eine Aufbewahrungsfrist für GA4, für BigQuery-Transfers oder für DV360 und CM360 brauchen, holen Sie sie aus der Dokumentation des jeweiligen Produkts.
Ein Detail wird leicht überlesen: 37 Monate sind länger als drei Jahre. Reach and Frequency verfällt deshalb rund einen Monat vor den Leistungsdaten zu denselben Kampagnen. Wenn Sie beides sichern, hat R&F die knappere Frist, nicht die großzügigere.
2. Die eigene Frist ausrechnen – und herausfinden, was schon weg ist
Die Grenze ist rollierend, kein einmaliger Schnitt im Juni. Rechnen Sie sie aus, statt sie sich zu merken:
from datetime import date
from dateutil.relativedelta import relativedelta
today = date(2026, 8, 10)
print(today - relativedelta(months=37)) # Leistungsdaten 2023-07-10
print(today - relativedelta(years=3)) # Reach and Frequency 2023-08-10
print(today - relativedelta(years=11)) # Änderungsverlauf 2015-08-10
Alles, was vor dem 10.07.2023 liegt, ist über die API bereits nicht mehr zu retten. Morgen ist es alles vor dem 11.07.2023. Bevor Sie eine Zeile Export-Code schreiben, sollten Sie wissen, wie viel des noch verfügbaren Zeitraums Sie tatsächlich vorliegen haben. Gegen ein Postgres-Warehouse mit einer Tagestabelle:
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;
Lassen Sie dieselbe Abfrage noch einmal laufen, diesmal mit Ihrem eigenen frühesten erwarteten Tag als Fensterbeginn. Alles, was sie unterhalb von 2023-07-10 meldet, ist ein dauerhaftes Loch; alles darüber ist Arbeit, die Sie noch erledigen können.
3. Ein Backfill-Job, der wieder aufsetzt
Eine GAQL-Feldliste drucke ich hier nicht ab, denn die richtige ist die, die Sie ohnehin schon verwenden. Nehmen Sie die Resource, das Datumssegment und die ausgewählten Felder direkt aus dem Report, den Sie heute fahren, legen Sie sie in eine Config-Datei – und lassen Sie dem Job nichts als Eigentum außer dem Datumsfenster:
{
"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"
}
Der Runner holt pro Aufruf genau ein Fenster und schreibt es fest. Der State besteht aus vier Spalten: customer_id, window_start, window_end, rows, geschrieben in derselben Transaktion wie die Daten – ein Absturz wiederholt also einfach das Fenster.
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 hing fest, diese Tage sind verfallen
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 ist Ihr vorhandener API-Wrapper, gleich welche Client-Bibliothek darunter liegt. Alles Übrige in der Datei arbeitet auf Ihrer Seite der Linie: Ihre Config, Ihre Tabelle, Ihre Transaktionsgrenze.
4. Cron, Reihenfolge und das Verlustregister
# /etc/cron.d/ads-backfill — ein Fenster pro Stunde, ältestes zuerst
7 * * * * ads /opt/ads-backfill/.venv/bin/python /opt/ads-backfill/backfill.py >> /var/log/ads-backfill.log 2>&1
Zwei Dinge an diesem Job sind Absicht. Er läuft von der Grenze aus vorwärts, nicht von heute aus rückwärts, denn nur die ältesten Daten haben eine Frist; die jüngsten Tage können warten. Und wenn er feststellt, dass die Grenze seinen eigenen Fortschritt überholt hat, überspringt er die Lücke nicht stillschweigend, sondern schreibt den Zeitraum in backfill_lost. Diese Tabelle ist die ehrliche Antwort auf die Frage „Welche Zeiträume haben wir nicht mehr?“ – und sie gehört auf ein Dashboard, direkt neben die Lückenabfrage aus Abschnitt 2.
Die Rechnung ist entspannt. Vom 10.07.2023 bis gestern sind es 1.126 Tage, also 161 Fenster zu je sieben Tagen. Bei einem Fenster pro Stunde sind das keine sieben Tage reale Laufzeit, in denen die Grenze um sieben Tage weiterrückt: Sie gewinnen 168 Tage Historie für jeden einzelnen, den Sie verlieren. Ist der Rückstand einmal abgearbeitet, hält derselbe Job das hintere Ende aktuell und kostet einen API-Call pro Stunde.
5. Zusammenfassung
Sie haben jetzt eine Grenze, die Sie berechnen statt sie sich zu merken, eine Abfrage, die Ihnen sagt, welche Tage innerhalb des noch verfügbaren Fensters in Ihrem Warehouse fehlen, einen Runner für jeweils ein Fenster, der nach einem Absturz sauber wieder aufsetzt und sich selbst auf den aktuellen Cutoff begrenzt, einen stündlichen Cron-Eintrag und ein Register der Zeiträume, die bereits verloren sind. Der gesamte Rückstand ab dem 10.07.2023 ist nach rund einer Woche unbeaufsichtigtem Betrieb abgearbeitet; danach wird der Job zur reinen Wartungsaufgabe.
Der Gewinn: Ihre Reporting-Historie hängt nicht länger an der Aufbewahrungsrichtlinie eines anderen. Jahresvergleiche, Saisonalitätsmodelle und Attributions-Rebuilds brauchen früher oder später Daten, die älter als 37 Monate sind, und nach dem 01.06.2026 existiert davon nur noch die Kopie, die Sie selbst vorhalten. Prüfen Sie die drei dokumentierten Fristen in der Primärquelle und ignorieren Sie die Zusatzzahlen, die mit ihnen kursieren: Google Ads data retention policy.