GA4 Data API: Why Your Report Came Back Truncated
Somewhere in a dashboard there is a number that is wrong by four percent and nobody knows it. The report loads. The API returned HTTP 200. The rows look plausible. What nobody checked is whether the API returned all the data it was asked for, and until recently no field told you.
Google fixed that in September 2026. The fix is one array in the response metadata, and it changes what "the report ran successfully" is allowed to mean.
A 200 response is not a complete response
The GA4 Data API answers the question it was given, within limits it does not put in the rows. Rows can be missing because a date range predates the property's retention setting. Events can be sampled down to a fraction. Dimension combinations can be collected into an (other) row. A quota can stop a request at the door. None of these produce an error in the response, and all of them change the total.
Practitioners usually find out sideways. A monthly figure does not match the GA4 interface. A client asks why the dashboard says 12,400 sessions when the property says 13,100. Someone spends an afternoon on dimension definitions before discovering the query was sampled.
What changed on 14 September 2026
The Data API changelog entry for that date added data truncation reasons to report responses. The field is dataTruncationReasons, it lives on ResponseMetaData, and it is available in both v1 beta and v1 alpha. When a report's data is truncated the field carries a list of DataTruncationReason objects, each holding the truncation type, a descriptive message, and the dates affected.
Before that, the only way to detect truncation was to compare totals against another source and hope the difference was small enough to notice. Now the response says it outright, and a report with no truncation reasons is one you can defend.
What sits inside a truncation reason
Four fields. dataTruncationType is the enum that names the cause. dataTruncationMessage is a readable sentence explaining it. dataTruncationDateRanges lists the affected spans as startDate and endDate, both inclusive and both formatted YYYY-MM-DD. dataTruncationDate gives a single cutoff date, and data before that date is the part that was dropped.
Read the ranges before the type. A report can be truncated for a reason that is entirely expected, such as an Ads metric beyond the 36 month window, and still be correct for the dates you actually care about.
The eleven types, sorted by who caused them
Each value below carries the DATA_TRUNCATION_TYPE_ prefix.
Your query. DATA_TRUNCATION_TYPE_DATE_RANGE means the query date range may not be fully served. This is the one you can usually fix, by narrowing the range, dropping a high cardinality dimension, or splitting the request in two.
The property's settings. DATA_TRUNCATION_TYPE_PROPERTY means the query reaches event data older than the retention date. Nothing in the request can recover it, because the data is gone.
An upstream product's policy. GOOGLE_ADS applies the 36 month Google Ads retention policy. DV360 and CM360 each apply a policy that does not permit data older than two years. These are not GA4 limits and they do not move when you ask nicely.
Metric age. ITEM_SCOPED_ECOMMERCE_METRICS and EVENT_SCOPED_ECOMMERCE_METRICS report that the newer ecommerce metrics only have data after a specific date, so ranges that start earlier show nothing for those metrics while older metrics look healthy.
Model start dates. RULES_BASED_MODELS and DATA_DRIVEN_ATTRIBUTION mark the golden date of the attribution report for each model family. A range that begins before the model existed returns partial attribution, and the message names the cutoff.
Labels that do less work. CONVERSIONS reports truncation in a conversions report, and UNSPECIFIED is the fallback when the API has no better name for the cause.
Retention decides what the API is allowed to return
GA4 retention is a property setting, and it applies to user level data tied to cookies, User-ID and advertising identifiers. Standard properties offer two months or fourteen months, and the same setting covers key events data. Other event data follows the same two options on a standard property. Analytics 360 adds 26, 38 and 50 months for that category. Google signals data tops out at 26 months whatever you choose.
Google Ads data inside GA4 has its own limit, a rolling 36 month window. Past that, reports show a banner and a data quality indicator, and at daily, weekly or hourly granularity the Ads metrics read zero for older dates. Monthly granularity behaves differently and can show complete months when the range aligns to calendar months. A metric that reads zero because of that rule is not broken.
There is a governance point here that agencies tend to skip. The retention setting is also a statement in your privacy notice. If you told users you delete user level data after fourteen months, then querying twenty four months through the API is asking for data you said you no longer hold. The truncation flag is the API agreeing with your own notice, and the honest response is to fix the query rather than extend retention to make a chart look complete.
Sampling is reported separately
Sampling does not appear in dataTruncationReasons. It has its own array, samplingMetadatas, with one entry per date range, in the order the ranges were requested. If the report was not sampled the field is absent rather than zero, so check for presence, not for a value.
Each entry carries samplesReadCount and samplingSpaceSize. Divide the first by the second and you have the share of available data that went into the answer. A report built on a quarter of the events is directionally useful and is not a billing figure.
Sampling starts when the events needed to answer a query exceed the property's limit. For event level queries that limit is 10 million events on a standard property and up to 1 billion on Analytics 360, with a default of 100 million per query on 360. The Help Centre is explicit that filtering a large dataset by country can trigger sampling even with expanded data requests, because the filter changes how the data is processed. Two dashboards on the same property can therefore disagree purely because one of them filters.
Unsampled distinct counts are not exact either. Google uses HyperLogLog++, sketch 14, and states that discrepancy is usually under one percent but can be higher when you combine more than one HLL++ metric. Users and sessions are estimates in both cases, just tighter ones.
The (other) row and the cardinality flag
When a table needs more rows than its limit allows, Analytics keeps the most common dimension values and collects the rest under an (other) row. Google's example is a Pages and screens table capped at 100,000 rows on a property with 150,000 unique pages, where the least common 50,000 pages end up in one row.
The API reports this with dataLossFromOtherRow. One detail in the reference matters for debugging: the flag is populated from the aggregated table used to build the report, so it stays true even when your request filters the (other) row out of the results. A report filtered to a single source can look complete while the flag says otherwise.
Google treats any dimension with more than 500 values as high cardinality and calls that guidance rather than a limit. Row limits vary by property type, by report and by query complexity. The practical rule is shorter: if you group by page path, a custom dimension or anything user generated, read the flag before you read the total.
Thresholding and restricted metrics
Two more metadata fields matter for compliance work. subjectToThresholding says the report was subject to data thresholds, which exist so nobody can infer an individual's demographics or interests from a small group. The reference makes a point that is easy to miss: a report can be subject to thresholding and still hold all its data, when every row sits above the threshold. The flag means the check ran.
schemaRestrictionResponse lists metrics restricted by data access rules, appearing as COST_DATA and REVENUE_DATA. If a cost metric reads null for one user and fine for another, this is where the answer is.
Five dimensions are potentially thresholded: userAgeBracket, userGender, brandingInterest, audienceId and audienceName. Properties get 120 potentially thresholded requests per hour, counted separately from token quota. A nightly job that pulls demographic breakdowns for forty clients can exhaust that budget without touching a single token.
emptyReason is the field to read when a report comes back with no rows at all. It explains the emptiness instead of leaving you to guess whether the property has no data or the request was malformed.
Quota, and the 429 that ends the run
Quota is the failure mode that looks like a bug. The API has four categories, Core, Realtime, Funnel and Chat, and each request charges one of them. Core tokens per property run at 200,000 per day on a standard property and 2,000,000 on 360. Per hour it is 40,000 and 400,000. Per project per property per hour, 14,000 and 140,000. Concurrent requests are capped at 10 and 50. Server error quota is 10 and 50 per project per property per hour, charged only when a request actually returns a 500 or 503.
Most requests cost ten tokens or fewer and complex ones cost more. The factors Google lists are row count, the number of dimensions and metrics, filter complexity, date range length, data cardinality and the property's event volume. A single charge is deducted from the day, hour and project hour buckets at once, so any one of them reaching zero returns 429 RESOURCE_EXHAUSTED for that property.
Set returnPropertyQuota: true in the request body and the response carries a PropertyQuota object with consumed and remaining for each bucket. That is the cheapest diagnostic in the API. Daily quotas reset at midnight Pacific time. Hourly quotas reset within an hour but not necessarily on the hour, which explains why a retry fails at 10:59 and succeeds at 11:20.
Two habits reduce the damage. Group up to five reports into one HTTP call with batchRunReports or batchRunPivotReports, remembering that each report still counts individually toward tokens and concurrency. And throttle background jobs when remaining tokens fall below a threshold you choose.
A five line check to put in front of every dashboard
Read the metadata before the rows. In Python, after decoding the response:
meta = response.get("metadata", {})
if meta.get("dataTruncationReasons"):
warn([r["dataTruncationType"] for r in meta["dataTruncationReasons"]])
if meta.get("samplingMetadatas"):
warn([int(s["samplesReadCount"]) / int(s["samplingSpaceSize"])
for s in meta["samplingMetadatas"]])
if meta.get("dataLossFromOtherRow"):
warn("high cardinality loss")
if meta.get("subjectToThresholding") or meta.get("emptyReason"):
warn("thresholded or empty")
Log the result next to the report name and the date it ran. A dashboard that prints "sampled at 63 percent" is doing its job. One that prints nothing while returning two thirds of the data is not.
When the API is the wrong tool
Three cases justify leaving the API behind rather than tuning it. You need per user or per event rows, which the API does not return. You need history older than the property's retention setting. Or you need every event without sampling because the numbers have to reconcile to the penny. That is the BigQuery export, which has its own costs and its own limits, and it is the right answer for a warehouse rather than a client dashboard.
For everything else the API is fine, provided you read the metadata and repeat what it says in the report. The truncation reasons field has been in the response since September 2026, so the excuse is gone. If a number ships to a client without that check, the four percent gap belongs to whoever wrote the query.
If your dashboards pull from the GA4 Data API and nobody has checked the response metadata since they were built, book a free tracking audit and we will read them with you.