Skip to main content

gemini-rotator

Production-grade Gemini API key rotation with automatic rate-limit handling, exponential backoff, per-key RPM tracking, per-request latency stats, and optional persistence.

pip install gemini-api-rotator

Why

Gemini's free tier enforces per-key rate limits and quota ceilings. With multiple keys you can spread load across them — but only if you handle the rotation, cooldowns, retries, and stats correctly. This library does all of that.

Key design goals:

  • Zero frictionawait rotator.execute(prompt) handles everything
  • Zero required dependencies — core library is pure stdlib; google-genai only needed for actual API calls
  • Persistent stats — per-request latency stored in SQLite (ships with Python)
  • Thread-safe — works in async apps, threaded workers, Telegram bots, FastAPI, anything

Install

# Core library only (no dependencies)
pip install gemini-api-rotator

# With google-genai so you can use execute()
pip install "gemini-api-rotator[genai]"

# With MongoDB adapter
pip install "gemini-api-rotator[mongo]"

# Everything
pip install "gemini-api-rotator[all]"

Quick start

import asyncio
from gemini_rotator import GeminiAPIRotator

rotator = GeminiAPIRotator([
    "AIza..key1..",
    "AIza..key2..",
    "AIza..key3..",
])

async def main():
    response = await rotator.execute("Explain async/await in one sentence.")
    print(response.text)

asyncio.run(main())

That's it. Key selection, retries, cooldowns, and stat recording happen automatically.


With SQLite persistence

Suspension state and latency stats survive restarts.

from gemini_rotator import GeminiAPIRotator
from gemini_rotator.adapters import SQLiteAdapter

db      = SQLiteAdapter("gemini_keys.db")
rotator = GeminiAPIRotator(keys, db=db)

With custom config

from gemini_rotator import GeminiAPIRotator, RotatorConfig

cfg = RotatorConfig(
    rpm_per_key            = 10,      # proactively skip a key after 10 calls/min
    cooldown_base_seconds  = 60,      # first rate-limit = 60s cooldown
    cooldown_max_seconds   = 1800,    # cap at 30 min after repeated hits
    max_concurrent_per_key = 3,       # max simultaneous requests per key
    max_retries            = 4,       # retries before raising
    retry_delay_seconds    = 1.0,
)

rotator = GeminiAPIRotator(keys, config=cfg)

Batch requests

prompts = ["Summarise X", "Translate Y", "Classify Z", ...]

responses = await rotator.execute_batch(
    prompts,
    model       = "gemini-2.5-flash",
    concurrency = 10,   # max simultaneous requests
)

for r in responses:
    if isinstance(r, Exception):
        print("failed:", r)
    else:
        print(r.text)

Manual key management

If you prefer to manage keys yourself:

key = rotator.get_next_working_key()
if key is None:
    raise RuntimeError("All keys suspended.")

rotator.acquire(key)   # track concurrency
try:
    response = client.models.generate_content(...)
    rotator.record_success(key, latency=elapsed, model="gemini-2.5-flash")
except Exception as e:
    rotator.record_outcome(key, exc=e)   # auto-classifies and routes
finally:
    rotator.release(key)

Error classification

record_outcome(key, exc=e) classifies the exception automatically:

Exception message contains Action
429, RESOURCE_EXHAUSTED, QUOTA mark_rate_limited() — exponential cooldown
CONSUMER_SUSPENDED, PERMISSION_DENIED, API_KEY_INVALID mark_suspended() — blacklisted until recheck
500, 503, INTERNAL, UNAVAILABLE record_error() — transient, retry same key
anything else record_error() — unknown

Alert hooks

import requests

def alert_slack(key: str):
    requests.post(SLACK_WEBHOOK, json={"text": f"Gemini key {key[-6:]} suspended!"})

rotator = GeminiAPIRotator(
    keys,
    on_key_suspended = alert_slack,
    on_key_recovered = lambda k: print(f"Key {k[-6:]} recovered."),
)

Automatic suspended-key recovery

Suspended keys are automatically probed on a schedule. Any key that starts responding again rejoins rotation immediately.

async def recheck_loop():
    while True:
        await asyncio.sleep(6 * 60 * 60)   # every 6 hours
        await rotator.revalidate_suspended_keys()

asyncio.create_task(recheck_loop())

Observability

Summary line

print(rotator.summary())
# Keys: 4 total | 2 active | 1 cooling | 1 suspended

Per-key status

for s in rotator.status():
    print(s.masked, s.state, s.cooldown_remaining, s.stats.avg_latency)

Each KeyStatus object:

Field Type Description
masked str AIza...abcd1234
state KeyState active / cooling / suspended
cooldown_remaining float Seconds left in cooldown
consecutive_limits int Consecutive rate-limit hits
rpm_used int Calls in the last 60s
stats.total_requests int Lifetime requests
stats.success_rate float 0.0 – 1.0
stats.avg_latency float Seconds (None if no data)
stats.latency_min/max float Seconds

Export as JSON

import json
print(json.dumps(rotator.export_stats(), indent=2))

Latency history (SQLite only)

rows = db.get_latency_history(key="AIza...", limit=100)
# [{"key": ..., "latency_ms": 312.4, "model": "gemini-2.5-flash", "ts": ...}, ...]

CLI

export GEMINI_API_KEYS="key1,key2,key3"
export GEMINI_ROTATOR_DB="gemini_keys.db"   # optional, default: ./gemini_rotator.db

gemini-rotator status           # per-key status table
gemini-rotator stats            # cumulative stats
gemini-rotator stats --json     # machine-readable
gemini-rotator latency          # recent per-request latency log
gemini-rotator latency --limit 100
gemini-rotator test             # probe every key against Gemini live
gemini-rotator add AIzaSy...    # add a key
gemini-rotator remove AIzaSy... # remove a key
gemini-rotator reset-cooldowns  # clear all cooldowns
gemini-rotator reset-stats      # reset all stats
gemini-rotator reset-stats AIzaSy...  # reset one key's stats

Example gemini-rotator status output:

  Keys: 4 total | 2 active | 1 cooling | 1 suspended

  KEY               STATE       COOLDOWN      RPM    REQUESTS    OK%   AVG ms
  ────────────────  ──────────  ─────────  ──────   ─────────  ─────  ───────
  AIza...abcd1234   ● active            —    3/15        1423   98%      312
  AIza...efgh5678   ● active            —    1/15         891   97%      289
  AIza...ijkl9012   ⏳ cooling        47s    0/15         234   85%      401
  AIza...mnop3456   ❌ suspended         —    0/15          89   71%      —

Custom DB adapter

Implement the DBAdapter protocol to use any backend (PostgreSQL, Redis, DynamoDB, …):

from gemini_rotator.adapters import DBAdapter
from gemini_rotator.models   import KeyStats
from typing import Optional, Set

class MyAdapter:
    def get_suspended_keys(self) -> Set[str]: ...
    def mark_key_suspended(self, key: str) -> None: ...
    def unmark_key_suspended(self, key: str) -> None: ...
    def record_request(self, key, event, latency=None, model=None) -> None: ...
    def get_stats(self, key: str) -> KeyStats: ...
    def get_all_stats(self) -> list[KeyStats]: ...
    def reset_stats(self, key: Optional[str] = None) -> None: ...

rotator = GeminiAPIRotator(keys, db=MyAdapter())

event is one of "success", "rate_limit", "suspended", "transient", "error".


Integrating with a Telegram bot

from telegram.ext import ApplicationBuilder
from gemini_rotator import GeminiAPIRotator
from gemini_rotator.adapters import SQLiteAdapter

db      = SQLiteAdapter("bot_keys.db")
rotator = GeminiAPIRotator(
    keys,
    db               = db,
    on_key_suspended = lambda k: logger.error("Key suspended: %s", k[-6:]),
)

async def handle_message(update, context):
    response = await rotator.execute(
        update.message.text,
        model      = "gemini-2.5-flash",
        max_tokens = 512,
    )
    await update.message.reply_text(response.text)

Running the tests

pip install pytest pytest-asyncio
pytest tests/ -v

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gemini_api_rotator-2.0.0.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gemini_api_rotator-2.0.0-py3-none-any.whl (25.9 kB view details)

Uploaded Python 3

File details

Details for the file gemini_api_rotator-2.0.0.tar.gz.

File metadata

  • Download URL: gemini_api_rotator-2.0.0.tar.gz
  • Upload date:
  • Size: 20.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for gemini_api_rotator-2.0.0.tar.gz
Algorithm Hash digest
SHA256 2c5f65dd8aaded6f24495aac82c54f9e07f90cb543732393ba426298a48eb985
MD5 e9e3c630550b1e782ce06cb6e3cdb750
BLAKE2b-256 84ba18cb7ed32565364db5b83f549b160be9808d1d86494e9cfacab07f3cd176

See more details on using hashes here.

File details

Details for the file gemini_api_rotator-2.0.0-py3-none-any.whl.

File metadata

File hashes

Hashes for gemini_api_rotator-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b46a7af7ef17e860ffd36298671f7558c3eb3276ebb958963e218f85208ce835
MD5 d9b49dcd22c55f955785f18671a3fc1c
BLAKE2b-256 ebdae499aa69f0916e091027e9b79e39436dc431b404d932b306439baf734d82

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page