ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-04-23Advanced

Keeping the Antigravity Python API Stable in Production — Retries, Timeouts, and Circuit Breakers That Actually Work

A deeply practical guide to keeping Python services built on the Google Gen AI SDK alive under real traffic. We cover retry, timeout, circuit breaker, rate limit, and cost budgeting patterns with runnable code from an Antigravity workflow.

python26antigravity435production71resilience7retry8circuit-breaker3rate-limit4

Premium Article

It's 2 AM, Slack starts pinging, and your production AI endpoint has been flat on its back for fifteen minutes. The cause is almost always one of two things: a transient 503 wave on Google Gen AI's side, or your own retry loop burning through your RPM allowance while the real API was briefly merely grumpy. Either way, the thing that failed was not the API — it was your resilience design.

When you're building Python agents or API servers in Antigravity, it's easy to ship a first version that works beautifully on your laptop and falls apart the moment real traffic hits it. A bare try: ... except: ..., a blanket three-retry loop, a single global httpx.AsyncClient(timeout=30) — none of these are wrong by themselves, but together they tend to produce the worst failure modes: silent retries, client timeouts, cost explosions, and the deeply unpleasant feeling that you're not sure why any of it is happening.

This article walks through a resilience stack I've iterated on across several production services that call the Google Gen AI SDK from Python, all developed inside Antigravity. I'll show you where things actually break, how to respond without overcorrecting, and which parts of the design matter the most. By the end, you should be able to look at your own service and tell — with some confidence — where the next outage will come from and what you'd do about it.

Classify Failures Before You Write a Single Retry Line

The most common mistake I see in "resilient" clients is treating every exception the same way. A blanket retry on Exception is worse than no retry at all: it turns your 400 Bad Request into a slow-motion cascade, and it disguises real bugs behind pretend transient errors.

When you call the Google Gen AI API, the failure modes you'll actually encounter fall into five buckets:

  • Transient service issues — HTTP 500/502/503/504. Recover in seconds to minutes. Safe to retry.
  • Rate limiting — HTTP 429. You've hit an RPM or TPM ceiling. Obey the Retry-After header.
  • Client-side errors — HTTP 400/401/403/404. Something about the request is wrong. Fail immediately.
  • Timeouts — connect, read, or end-to-end. Could be network, could be the model thinking for a long time. You need to know which.
  • Content filter failuresFinishReason.SAFETY, FinishReason.RECITATION. Retrying without changing the prompt is pointless. Never retry.

Only the first two are safe to retry blindly. Everything else needs a different response. The simplest way to enforce this at the code level is to define an exception hierarchy up front and let your retry library branch on it.

Start with a Well-Shaped Exception Hierarchy

Ask the Antigravity agent for "a small exception taxonomy that separates retryable from permanent AI API failures," and you get a template you can extend. Here's the shape I keep coming back to:

# ai_errors.py — retry-aware exception hierarchy
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
 
class AIError(Exception):
    """Base class for all AI API failures."""
 
class RetryableAIError(AIError):
    """Transient failure. Safe to retry with exponential backoff."""
 
class RateLimitedError(RetryableAIError):
    """HTTP 429. Respect retry_after when provided."""
 
    def __init__(self, message: str, retry_after: Optional[float] = None):
        super().__init__(message)
        self.retry_after = retry_after
 
class PermanentAIError(AIError):
    """Request is malformed or semantically invalid. Do not retry."""
 
@dataclass
class SafetyBlocked(PermanentAIError):
    """Blocked by content filter — prompt must be revisited."""
    reason: str
 
    def __str__(self) -> str:
        return f"Blocked by safety filter: {self.reason}"

The rest of the stack — retry loops, circuit breakers, metrics — is built on top of this taxonomy. Once you enforce "only RetryableAIError triggers retries," roughly 70% of the "retry storm" patterns I've seen in production code disappear on their own.

Exponential Backoff + Jitter, Without the Shortcuts

Fixed-interval retries are a classic anti-pattern. If ten instances all hit a 503 at the same moment and each retries in exactly one second, you'll all slam the API together the instant it recovers — exactly when it's most fragile. This is the "thundering herd" problem, and jitter is the cure.

The pattern I use in Python is tenacity on top of httpx. Tenacity lets you express the policy declaratively, so your business logic isn't drowning in retry plumbing.

# resilient_client.py — tenacity-based retry with exponential backoff + jitter
import asyncio
import logging
import random
import httpx
from tenacity import (
    AsyncRetrying,
    retry_if_exception_type,
    stop_after_attempt,
    stop_after_delay,
    wait_exponential_jitter,
    before_sleep_log,
)
 
from ai_errors import (
    AIError,
    PermanentAIError,
    RateLimitedError,
    RetryableAIError,
)
 
logger = logging.getLogger(__name__)
 
def _classify_httpx_error(exc: httpx.HTTPStatusError) -> AIError:
    status = exc.response.status_code
    if status == 429:
        retry_after = exc.response.headers.get("retry-after")
        return RateLimitedError(
            f"Rate limited: {exc.response.text[:200]}",
            retry_after=float(retry_after) if retry_after else None,
        )
    if status in (500, 502, 503, 504):
        return RetryableAIError(f"Transient {status}: {exc.response.text[:200]}")
    return PermanentAIError(f"{status}: {exc.response.text[:200]}")
 
async def call_with_retry(
    client: httpx.AsyncClient,
    request: httpx.Request,
    *,
    max_attempts: int = 5,
    total_budget_sec: float = 20.0,
) -> httpx.Response:
    """Call the Gen AI API with exponential backoff + jitter.
 
    - max_attempts: includes the first call
    - total_budget_sec: hard cap across all attempts
    - Honors Retry-After on 429
    - Re-raises PermanentAIError immediately
    """
    async for attempt in AsyncRetrying(
        reraise=True,
        stop=stop_after_attempt(max_attempts) | stop_after_delay(total_budget_sec),
        wait=wait_exponential_jitter(initial=1, max=8, jitter=1.5),
        retry=retry_if_exception_type(RetryableAIError),
        before_sleep=before_sleep_log(logger, logging.WARNING),
    ):
        with attempt:
            try:
                response = await client.send(request)
                response.raise_for_status()
                return response
            except httpx.HTTPStatusError as exc:
                err = _classify_httpx_error(exc)
                if isinstance(err, RateLimitedError) and err.retry_after is not None:
                    logger.warning(
                        "429 received; sleeping %.2fs per Retry-After",
                        err.retry_after,
                    )
                    await asyncio.sleep(err.retry_after + random.uniform(0, 0.5))
                    raise RetryableAIError("Retryable after 429") from exc
                raise err from exc
            except (httpx.ConnectError, httpx.ReadTimeout) as exc:
                raise RetryableAIError(f"Network error: {exc}") from exc

Four things in that code matter more than they might look.

First, wait_exponential_jitter(initial=1, max=8, jitter=1.5) gives you backoffs of roughly 1, 2, 4, 8 seconds, each scrambled by up to 1.5 seconds of jitter. Even if dozens of workers retry at the same time, they desynchronize naturally.

Second, combining stop_after_attempt with stop_after_delay means you stop when either the count or the time budget runs out. Without the time cap, a single slow attempt can push the whole retry window past any user-facing deadline.

Third, retry_if_exception_type(RetryableAIError) is where the earlier exception hierarchy pays off. PermanentAIError simply escapes the retry loop — no boilerplate needed.

Fourth, 429 is the odd one out. Exponential jitter is the wrong tool when the server has already told you exactly how long to wait. Respecting Retry-After (plus a tiny randomization to prevent everyone from returning at the same instant) is strictly better than computing your own schedule.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
You'll learn how to survive the classic 'middle-of-the-night 503 cascade' and the equally classic 'retry storm that doubled our bill overnight' — with concrete defensive patterns you can drop into your Antigravity Python projects today
You will be able to implement exponential backoff with jitter, circuit breakers, and budget-aware rate limiters on top of tenacity, httpx, and asyncio, and you'll understand exactly which knob to turn when production behavior deviates from your mental model
From streaming-aware resumption and cost-capped retries to a solid OpenTelemetry story, you'll walk away able to design AI API services that stay calm under pressure and that you're genuinely comfortable putting on-call for
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Agents & Manager2026-04-13
Resilient AI Agents in Antigravity — Retry, Circuit Breakers, and Fallback Strategies for Production
Build fault-tolerant AI agents in Antigravity with production-grade retry strategies, circuit breakers, model fallback chains, and checkpoint recovery. Every pattern you need to keep agents running reliably.
Integrations2026-04-22
Three Safeguards Every Antigravity Python API Deployment Needs Before Production
Retries, timeouts, and circuit breakers — the three production safeguards you need around your Antigravity Python API calls, with working code for each.
App Dev2026-06-28
When Streaming Works Locally but Arrives All at Once in Production — Field Notes on Proxy Buffering and Silent Stalls
Stream Gemini through Antigravity over SSE and it flows token-by-token on localhost, then freezes for seconds and dumps the whole answer in production. Field notes on measuring the stall first, then killing proxy buffering, idle disconnects, and reconnect-driven double generation.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →