gquota▊

Drop-in rate-limit layer for the Gemini API free tier.
Backoff 4/8/16s + jitter · TTL response cache · RPM pacing · tolerant JSON recovery.

pip install gquota zero dependencies python ≥ 3.9 MIT 34 tests
GitHub → Sponsor ♥

one import, zero 429s

scan.py — live
from gquota import gemini

r = gemini.generate("gemini-2.5-flash", "P(typhoon lands this week)? JSON only")
r.text     # plain text
r.data     # {'p': 0.42, 'confidence': 0.8, ...} — recovered tolerantly
r.cached   # True → zero quota spent

the war-story: three scans, one key, mutual 429

Delphi, a prediction-market agent, runs three concurrent scans on one free-tier key (~10 RPM). The afternoon all three fired in the same minute:

scan.log — 15:59:47 — the stampede
15:59:47 open markets: 15
15:59:47 MARKET 0x2c178e ... implied=? model=FAIL
15:59:47 MARKET 0xb8fcc2 ... implied=? model=FAIL
15:59:47 MARKET 0x76225f ... implied=? model=FAIL
   ... 15/15 FAIL, all in the same second

# four minutes later — same logic, hardened call layer:
16:03:34 open markets: 15
16:03:37 0x2c178e ... model=0.480@gemini-flash-latest  →pass
16:03:51 0x76225f ... model=0.459@gemini-3-flash-preview →pass
16:04:01 0xb4ded8 ... model=0.170@gemini-3-flash-preview →pass

No market-logic changes — only the call layer. gquota is that layer, extracted, generalized, and tested with 34 mocked-HTTP tests.

the four layers

01 Backoff + jitter

4s → 8s → 16s with 0–1.5s uniform jitter on 429/503. Jitter is what breaks the lockstep: concurrent processes stop colliding on retry.

02 TTL response cache

Identical (model, prompt) pairs never re-burn quota. 2h default — tuned when the 4h cron kept re-pricing unchanged questions.

03 RPM pacing

6.5s minimum between live calls (~9 RPM). Staying under the cap proactively beats reacting to 429s after the fact.

04 Tolerant JSON recovery

Gemini sometimes emits unescaped quotes mid-string. Strict parse → strip fences → extract {…} → per-field regex salvage. Quota already spent; don't waste the answer.

API

callwhat it does
gemini.generate(model, prompt)Everything handled: cache → pacing → backoff → fallback → recovery
gemini.configure(rpm=…, …)One-time setup: key, RPM, TTL, backoff schedule, fallback ladder
Provider(post=…)Endpoint-agnostic core; injectable transport (how the tests mock HTTP)
provider.statsLive counters: calls, retries, cache_hits, pace_sleeps
RateLimitError / RetryExhaustedExplicit failure modes, never silent None

install

shell
pip install git+https://github.com/jtabsbm/gquota.git