
Building an LLM Cost Dashboard in BigQuery That Actually Changes Behavior
The end-to-end blueprint for tracking every LLM call in BigQuery — schema, ingestion patterns, cost attribution by team/feature/user, unit-economics dashboards, and the alerts that catch runaway spend before finance does.
LLM spend explodes silently. Provider dashboards show totals; they don't tell you which feature, which team, or which user is burning money. This article is the complete blueprint: an event schema that captures every call, ingestion patterns for OpenAI/Anthropic/Bedrock/Vertex, BigQuery models that compute cost per feature and per user, dashboards that surface anomalies, and alerts that fire before the bill does. Same pattern I use to keep LLM spend predictable across half a dozen production systems.
- Emit a structured log event for every LLM call — provider dashboards are not enough.
- Attribute cost via three dimensions minimum: feature, team, user.
- Materialize daily rollups in dbt; query hot data cheaply.
- Alert on rate-of-change, not just absolute spend.
- Track cost per successful action, not just per call — that's the unit economics number.
- Route non-critical calls to cheaper models when cost/action exceeds a threshold.
Why provider dashboards are not enough
OpenAI, Anthropic, and Bedrock all show a total. They don't answer:
- Which product feature spent the most this week?
- Which team is responsible?
- Which users are 10× the average?
- What's our cost per successful support ticket resolved?
- Which prompts are costing us in retries?
To answer those, you need your own event stream in a warehouse where you can join spend to product events.
The event schema
CREATE TABLE llm_events (
event_id STRING NOT NULL,
ts TIMESTAMP NOT NULL,
provider STRING NOT NULL, -- openai, anthropic, bedrock, vertex, self
model STRING NOT NULL, -- gpt-4o-mini, claude-3-5-sonnet, ...
operation STRING NOT NULL, -- chat, embed, image, transcribe
feature STRING NOT NULL, -- 'support-triage', 'summary', ...
team STRING NOT NULL, -- 'growth', 'support', 'internal'
user_id STRING, -- hashed
session_id STRING,
request_id STRING, -- provider's request id
prompt_tokens INT64,
completion_tokens INT64,
cached_tokens INT64, -- if provider supports prompt caching
latency_ms INT64,
status STRING, -- ok, error, timeout, rate_limited
error_code STRING,
cost_usd NUMERIC, -- computed at emit time
metadata JSON
)
PARTITION BY DATE(ts)
CLUSTER BY feature, team, model;Ingestion patterns
Three patterns, in order of preference:
- Direct BigQuery streaming insert from your app — simplest, works up to ~10k events/sec.
- Pub/Sub → Dataflow → BigQuery — buffered, retryable, decouples ingestion from app.
- Cloud Logging → BQ Log Sink — free-ish, but log-based cost quirks; good for low volume.
Wrap every LLM SDK call in a small helper that computes cost and emits the event:
def call_llm(messages, feature: str, team: str, user_id: str, **kw):
t0 = time.perf_counter()
try:
resp = client.chat.completions.create(messages=messages, **kw)
status = "ok"; err = None
usage = resp.usage
except APIError as e:
status, err = "error", e.code
usage = None; raise
finally:
emit_llm_event(
provider="openai", model=kw["model"], operation="chat",
feature=feature, team=team, user_id=hash_user(user_id),
prompt_tokens=usage.prompt_tokens if usage else 0,
completion_tokens=usage.completion_tokens if usage else 0,
latency_ms=int((time.perf_counter()-t0)*1000),
status=status, error_code=err,
cost_usd=compute_cost(kw["model"], usage) if usage else 0,
)
return respComputing cost correctly
Model prices change monthly. Keep a versioned price table in BigQuery joined by (model, effective_date). Never hardcode prices in application code — you'll get it wrong.
CREATE TABLE llm_prices (
provider STRING, model STRING,
effective_from DATE, effective_to DATE,
input_per_1k NUMERIC, cached_input_per_1k NUMERIC,
output_per_1k NUMERIC
);dbt models for rollups
-- models/marts/llm_daily.sql
{{ config(materialized='incremental', partition_by={'field':'day','data_type':'date'}) }}
SELECT
DATE(ts) AS day,
feature, team, model, status,
COUNT(*) AS calls,
SUM(prompt_tokens) AS prompt_tokens,
SUM(completion_tokens) AS completion_tokens,
SUM(cost_usd) AS cost_usd,
APPROX_QUANTILES(latency_ms, 100)[OFFSET(95)] AS p95_ms
FROM {{ ref('llm_events') }}
{% if is_incremental() %}
WHERE DATE(ts) >= DATE_SUB(CURRENT_DATE(), INTERVAL 3 DAY)
{% endif %}
GROUP BY 1,2,3,4,5Full dbt patterns in dbt in anger.
Unit economics: cost per successful action
Total spend divided by user count is a vanity metric. The real KPI is cost per successful business outcome.
WITH resolved AS (
SELECT session_id, COUNT(*) AS n
FROM support_events
WHERE outcome = 'resolved' AND DATE(ts) = CURRENT_DATE() - 1
GROUP BY 1
),
llm AS (
SELECT session_id, SUM(cost_usd) AS cost
FROM llm_events
WHERE feature = 'support-triage' AND DATE(ts) = CURRENT_DATE() - 1
GROUP BY 1
)
SELECT
SUM(llm.cost) / SUM(resolved.n) AS cost_per_resolution
FROM llm JOIN resolved USING (session_id);Dashboards that drive action
Build in Metabase, Looker, or Superset — five panels:
- Daily spend, stacked by feature; 7-day and 30-day trend.
- Cost per successful action, by feature.
- Top 10 users by cost (helps catch abuse or investigate power users).
- Model mix — % of spend on premium vs cheap models by feature.
- Error rate by (model, feature) — retries silently double cost.
Anomaly alerts that fire before finance calls
-- Runs hourly via scheduled query; posts to Slack via a webhook
WITH last_hour AS (
SELECT feature, SUM(cost_usd) AS cost
FROM llm_events
WHERE ts BETWEEN TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR) AND CURRENT_TIMESTAMP()
GROUP BY 1
),
baseline AS (
SELECT feature, AVG(hourly_cost) AS avg_cost, STDDEV(hourly_cost) AS sd
FROM (
SELECT feature, TIMESTAMP_TRUNC(ts, HOUR) AS h, SUM(cost_usd) AS hourly_cost
FROM llm_events
WHERE ts BETWEEN TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 14 DAY)
AND TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 DAY)
GROUP BY 1,2
)
GROUP BY 1
)
SELECT l.feature, l.cost, b.avg_cost, (l.cost - b.avg_cost) / b.sd AS z_score
FROM last_hour l JOIN baseline b USING (feature)
WHERE (l.cost - b.avg_cost) / b.sd > 4.0Any z-score >4 pings Slack. Beats waking up to a $2k bill.
Routing decisions from the dashboard
When cost-per-action for a feature exceeds threshold, take action:
- Route non-critical calls to a cheaper model (e.g., 4o → 4o-mini for summaries).
- Add prompt caching where the same system prompt is reused.
- Trim system prompts (measure per-call token count in your dashboard).
- Consider fine-tuning — see fine-tuning vs RAG.
- Add semantic caching for repeated queries (redis + embedding similarity).
Retention and privacy
- Never log prompt/response text by default — store hashes or truncated redactions only.
- If you must store text for eval, use a separate table with stricter IAM and 30-day TTL.
- Hash user IDs at emit time; keep the mapping in a locked table.
- Set BigQuery table expiration (90 days for events, 400 for daily rollups).
BigQuery cost of the LLM cost dashboard
Meta-observation: your observability data can itself become expensive. Partition by day, cluster by feature, alias every query to hit clustered columns. See BigQuery cost control that scales for the whole discipline.
What great looks like
- Every LLM call in the system emits an event within 100ms.
- The dashboard opens in under 2 seconds — always fresh, always cheap.
- Every product team can self-serve their spend and cost-per-outcome.
- Anomalies fire in Slack within an hour, not a month.
- Cost-per-action is a first-class product metric, tracked over time.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
BigQuery Cost Control That Scales: 12 Patterns from Real Warehouses
Twelve battle-tested patterns to cut BigQuery spend 30–70% without touching data quality — partitioning, clustering, materialized views, reservations, and the FinOps loop that keeps savings from decaying.
Observability for AI Systems: Traces, Prompts, Tokens, and the SLOs That Actually Matter
A 30-minute production guide to instrumenting LLM applications — what to log (and what never to), how to trace agent calls end-to-end, the four golden signals for AI, drift detection on outputs, and the dashboards you actually check at 2 a.m.
The Modern Data Stack in 2026: What Actually Matters (and What to Skip)
A no-hype 30-minute tour of the 2026 data stack: ingestion, warehouse, transformation, orchestration, semantic layer, reverse ETL, observability, and the AI layer on top — with concrete tool picks, cost anchors, and the anti-patterns that quietly eat budgets.
Browse every article by Islam Gamal on the author page.