Cut Google Ads API quota usage
If you’re getting RESOURCE_EXHAUSTED errors from Google Ads, or
just hitting your daily-operations limit in tight authoring loops,
this page is for you.
Untracked campaigns cost you nothing
bidsmith only touches what you declare. A campaign with no matching
.bid resource is never modified, deleted, or sent as a change —
bidsmith only acts on resources it manages (the ones carrying its
label). So a pile of paused or obsolete campaigns you don’t track
costs you nothing: no per-campaign API calls, no mutate operations,
no risk of being changed.
They’re still included in the one account-wide read bidsmith does on
each plan — but that read is a fixed set of SearchStream
queries regardless of how many campaigns the account holds. Five
campaigns or five thousand, the number of queries is the same; extra
campaigns only make the response payload a little larger. You don’t
need to delete or hide old campaigns to keep bidsmith fast or cheap.
What bidsmith already does
Every plan or apply writes the live state it pulls from Google
Ads to .bidsmith/cache/live-state.json in your project, along with
the OAuth access token in .bidsmith/cache/token.json. The next
plan or apply within the cache window reuses both — no
SearchStream queries, no OAuth round-trip.
The defaults:
- Live-state cache TTL: 15 minutes.
- Access-token cache: reused until 60 seconds before its server-issued expiry (typically ~1 hour).
- Whenever
applymutates the account: the live-state cache is dropped as the change is sent, not after it comes back — so even a timeout or an interrupted run leaves no stale copy behind. The nextplanstarts from real data. .bidsmith/is gitignored by default — nothing to commit.
Every plan, apply, and drift says out loud which account it read,
when, and where the answer came from — one line, same shape either way:
plan: live state for customers/1234567890 read 4m12s ago (cached — --refresh-state to refetch).plan: live state for customers/1234567890 read just now (fresh read).That line is the answer to “am I looking at current data?”, so you never have to infer it from what the output doesn’t say.
When to bust the cache
You normally don’t need to. The 15-minute TTL is short enough that
genuinely stale data is rare, and apply invalidates the cache when it
mutates. But you might want a fresh fetch if:
- Someone edited the account in the Google Ads UI mid-session.
- Someone else — a colleague, or your CI — applied to the same account
from a different machine. Their
applycleared their cache, not yours. - You’re chasing a “this looks wrong” diff and want to rule out stale state.
- You’re about to apply something high-stakes and want a final confirmation against live.
bidsmith plan --refresh-state .bidsmith apply --refresh-state .--refresh-state skips the read but still writes the result back to
the cache, so subsequent invocations get fresh data for free.
Planning fully offline
bidsmith plan --offline reads the cache only and prints the diff
without any network call — no SearchStream, no OAuth, no
validateOnly mutate. Useful for:
- Tight authoring loops where each
planwould cost an operations-quota mutate. - Offline work (planes, trains, sketchy hotel Wi-Fi).
- CI pre-flight on every PR push, with a nightly cache refresh job
doing the actual
pull.
# Warm the cache once.bidsmith pull -o /dev/null # or just run any plan
# Iterate locally.bidsmith plan --offline .# … edit .bid …bidsmith plan --offline .# … edit .bid …bidsmith plan --offline .
# When you're ready, do a real plan against live and apply.bidsmith plan .bidsmith apply .The offline summary is clearly marked so you don’t confuse it with a server-validated plan:
Plan: 3 to create, 1 to update, 0 to destroy, 12 unchanged. (offline — diff only, not server-validated)If the cache is missing or older than the TTL, --offline errors
out with a hint to run bidsmith pull (or just one normal plan)
to warm it.
Tuning the TTL
# Default: 900 seconds (15 minutes).BIDSMITH_CACHE_TTL_SECS=3600 bidsmith plan . # 1-hour TTLBIDSMITH_CACHE_TTL_SECS=60 bidsmith plan . # 1-minute TTLA longer TTL is fine on single-author accounts where bidsmith is the only thing touching Google Ads. Keep it short on shared accounts where someone might edit in the UI between your invocations.
Bypassing the cache entirely
BIDSMITH_NO_CACHE=1 bidsmith plan .Disables both caches (live-state and OAuth) for that invocation —
no reads, no writes. Equivalent to bidsmith before the cache
existed. Useful for debugging “is the cache lying to me?” without
deleting cache files by hand.
The pull-once, plan-many pattern
For long authoring sessions:
-
Warm the cache once at the start.
Terminal window bidsmith pull -o /tmp/snapshot.jsonpullalways fetches fresh from the API and writes.bidsmith/cache/live-state.jsonas a side effect, so the nextplanis cache-served. -
Iterate with
--offline. Zero API calls per cycle.Terminal window bidsmith plan --offline . -
Switch to a live plan when you’re satisfied. This is the one that calls validateOnly to catch server-side issues:
Terminal window bidsmith plan . -
Apply when the live plan looks right:
Terminal window bidsmith apply .applyclears the cache as it mutates, so the next session starts clean.
A 10-edit authoring loop with this pattern costs ~3 API calls
(1 initial pull + 1 final live plan + 1 apply mutate) instead of
~40 (each plan would otherwise issue 12 SearchStream reads + 1
validateOnly mutate).
CI patterns
A common quota-killer is bidsmith plan on every PR push.
Suggested wiring:
- Nightly job that runs
bidsmith pull -o cache/snapshot.jsonand commits the snapshot to a side branch (or stores it as a CI artifact / S3 object). - PR job that downloads the snapshot, drops it into
.bidsmith/cache/live-state.json, and runsbidsmith plan --offline. Zero API calls per PR. - Merge-to-main job that runs a real
bidsmith planonce, thenbidsmith apply --auto-approve— that’s the only place the real API is contacted.
The snapshot can be regenerated more often than nightly if your account changes frequently; the only cost is one round of SearchStream queries.
Where the cache lives
Everything in .bidsmith/cache/:
| File | Contents | Lifetime |
|---|---|---|
live-state.json | Raw SearchStream batches keyed by customer + version | TTL (default 15 min) |
token.json | OAuth access token + expiry (mode 0600) | ~1 hour, server-issued |
last-mutate.json | When this copy last applied, and to which account | Until the next apply |
All three are JSON, all three are gitignored, all three are safe to
delete by hand — rm -rf .bidsmith/cache/ is a valid “reset
everything” command.
See also
- A plan reported errors that turned out not to be real — how to tell a stale snapshot from a genuinely broken change.
bidsmith plan— the verb that benefits most from the cache.bidsmith pull— the explicit cache-warming command.- Plan and apply — the underlying loop the cache speeds up.