Google-Ads-Datenaufbewahrung: 37 Monate Leistungsdaten und ein wiederaufnehmbarer Backfill-Job

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 noch abrufbare Leistungstag ist damit der 10.07.2023 – und diese Linie rückt jeden Tag um einen Tag weiter. Liegt die Reporting-Historie ausschließlich in Google Ads, geht davon still und leise pro Tag ein Tag verloren. 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 ins eigene 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. Die Planung stützt sich auf die drei Werte oben. Eine Aufbewahrungsfrist für GA4, für BigQuery-Transfers oder für DV360 und CM360 gehört aus der Dokumentation des jeweiligen Produkts genommen.
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. Wird beides gesichert, 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. Diese Grenze gehört ausgerechnet, nicht auswendig gelernt:
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. Vor der ersten Zeile Export-Code gehört geklärt, wie viel des noch verfügbaren Zeitraums tatsächlich lokal vorliegt. 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;
Dieselbe Abfrage läuft anschließend noch einmal, diesmal mit dem eigenen frühesten erwarteten Tag als Fensterbeginn. Alles, was sie unterhalb von 2023-07-10 meldet, ist ein dauerhaftes Loch; alles darüber ist noch zu retten.
3. Ein Backfill-Job, der wieder aufsetzt
Eine GAQL-Feldliste drucke ich hier nicht ab, denn die richtige ist die ohnehin schon verwendete. Resource, Datumssegment und ausgewählte Felder kommen direkt aus dem Report, der heute läuft, und wandern in eine Config-Datei – dem Job selbst bleibt 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 der vorhandene API-Wrapper, gleich welche Client-Bibliothek darunter liegt. Alles Übrige in der Datei arbeitet auf der eigenen Seite der Linie: eigene Config, eigene Tabelle, eigene 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: Das sind 168 gewonnene Tage Historie für jeden einzelnen verlorenen. Ist der Rückstand einmal abgearbeitet, hält derselbe Job das hintere Ende aktuell und kostet einen API-Call pro Stunde.
5. Zusammenfassung
Damit steht eine Grenze, die berechnet statt gemerkt wird, eine Abfrage, die nennt, welche Tage innerhalb des noch verfügbaren Fensters im Warehouse fehlen, und einen Runner für jeweils ein Fenster, der nach einem Absturz sauber wieder aufsetzt und sich selbst auf den aktuellen Cutoff begrenzt. Dazu kommen ein stündlicher 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: Die 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 selbst vorgehaltene Kopie. Die drei dokumentierten Fristen gehören in der Primärquelle geprüft, die mit ihnen kursierenden Zusatzzahlen ignoriert: Google Ads data retention policy.
2 Kommentare
Die Trennung zwischen den drei belegten Fristen und den Zahlen, die nur daneben kursieren, ist der wertvollste Teil — bei uns stand die 24-Monats-Angabe bereits in einer Planung.
Eine Konsequenz daraus ist mir erst beim zweiten Lesen aufgefallen: Reach und Frequency laufen früher ab als die Leistungsdaten. Ändert das die Reihenfolge beim Sichern?
Es ändert sie, und zwar gegen die Intuition. Drei Jahre sind knapper als siebenunddreissig Monate, also verliert Reach und Frequency zuerst — obwohl die Leistungsdaten die prominentere Frist tragen und deshalb zuerst gesichert werden.
Praktisch heisst das: zwei Läufe mit zwei eigenen Grenzen, nicht ein Lauf mit der grosszügigeren. Wer beide Datensätze durch dieselbe Rechnung schickt, sichert Reach und Frequency einen Monat lang gegen eine Grenze, die für sie gar nicht gilt — und stellt am Ende fest, dass genau der älteste Teil fehlt.
Der Rest der Mechanik bleibt gleich: von der Grenze nach vorn arbeiten statt von heute nach hinten, und die Spanne, die trotzdem verlorengegangen ist, in einer eigenen Tabelle festhalten. Bei zwei Datensätzen gehört in diese Tabelle zusätzlich die Angabe, um welchen es geht — sonst steht dort später eine Lücke ohne Zuordnung, und die Frage, was fehlt, beginnt von vorn.