Problem
BigQueryAgentAnalyticsPlugin can write duplicate rows when its internal BigQuery Storage Write API request is retried after an ambiguous failure.
This concerns retries of the BigQuery write, not retries by the agent, model, or tools.
The plugin writes to BigQuery's _default stream. BatchProcessor._write_rows_with_retry constructs one AppendRowsRequest and resends the same serialized batch following transient errors or its 30-second
client-side timeout.
If BigQuery accepts the first append but its acknowledgement is lost or delayed, the plugin retries the batch. Because _default provides at-least-once delivery, BigQuery can accept it again and produce duplicate rows.
Relevant code:
Impact
We observed byte-identical duplicate analytics rows in production, including identical timestamps, trace/span IDs, token usage, status, and content.
The issue is rare. Over seven days, six duplicate LLM_RESPONSE rows inflated a naive SUM(usage.total) by 182,555 tokens, approximately 0.31% of the total. This can silently over-count tokens, costs, latency, and other metrics derived from the analytics table. However this was for a poc agent with few users and can easily inflate with more use.
Proposed fix
BigQuery does not allow append offsets on _default, and BigQueryWriteAsyncClient has no exactly_once option.
Exactly-once append behaviour requires an application-created COMMITTED stream with offsets:
- Create one committed stream per
BatchProcessor.
- Include the current offset in each
AppendRowsRequest.
- Retry an ambiguous append using the same stream and offset.
- Advance the offset after success.
- Treat
ALREADY_EXISTS at that offset as confirmation that the earlier
attempt succeeded.
- Handle unexpected
OUT_OF_RANGE without blindly resending on a new stream.
A committed stream preserves immediate row visibility while making Storage Write API retries idempotent.
This fix would prevent duplicates caused by BigQuery write retries. It would not deduplicate genuinely separate events emitted by the agent or application.
For anyone familiar with Apache beam this is similar at a high level to their use_at_least_once=False option for BigQuery Storage Write API sinks. Beam provides the stream, offset, and durable checkpoint management behind that abstraction; because ADK uses BigQueryWriteAsyncClient directly, the plugin must manage its committed stream and append offsets itself.
References:
Problem
BigQueryAgentAnalyticsPlugincan write duplicate rows when its internal BigQuery Storage Write API request is retried after an ambiguous failure.This concerns retries of the BigQuery write, not retries by the agent, model, or tools.
The plugin writes to BigQuery's
_defaultstream.BatchProcessor._write_rows_with_retryconstructs oneAppendRowsRequestand resends the same serialized batch following transient errors or its 30-secondclient-side timeout.
If BigQuery accepts the first append but its acknowledgement is lost or delayed, the plugin retries the batch. Because
_defaultprovides at-least-once delivery, BigQuery can accept it again and produce duplicate rows.Relevant code:
BatchProcessor._write_rows_with_retryImpact
We observed byte-identical duplicate analytics rows in production, including identical timestamps, trace/span IDs, token usage, status, and content.
The issue is rare. Over seven days, six duplicate
LLM_RESPONSErows inflated a naiveSUM(usage.total)by 182,555 tokens, approximately 0.31% of the total. This can silently over-count tokens, costs, latency, and other metrics derived from the analytics table. However this was for a poc agent with few users and can easily inflate with more use.Proposed fix
BigQuery does not allow append offsets on
_default, andBigQueryWriteAsyncClienthas noexactly_onceoption.Exactly-once append behaviour requires an application-created
COMMITTEDstream with offsets:BatchProcessor.AppendRowsRequest.ALREADY_EXISTSat that offset as confirmation that the earlierattempt succeeded.
OUT_OF_RANGEwithout blindly resending on a new stream.A committed stream preserves immediate row visibility while making Storage Write API retries idempotent.
This fix would prevent duplicates caused by BigQuery write retries. It would not deduplicate genuinely separate events emitted by the agent or application.
For anyone familiar with Apache beam this is similar at a high level to their use_at_least_once=False option for BigQuery Storage Write API sinks. Beam provides the stream, offset, and durable checkpoint management behind that abstraction; because ADK uses BigQueryWriteAsyncClient directly, the plugin must manage its committed stream and append offsets itself.
References:
AppendRowsRequest.offset