{"record":{"id":"acec5e18f2fc912a","repo":"headroomlabs-ai/headroom","slug":"rate-limited-retry-after-wait-seconds-1f-s-acec5e","errorCode":null,"errorMessage":"Rate limited. Retry after {wait_seconds:.1f}s","messagePattern":"Rate limited\\. Retry after (.+?)s","errorType":"http","errorClass":"HTTPException","httpStatus":429,"severity":"warning","filePath":"headroom/proxy/handlers/gemini.py","lineNumber":398,"sourceCode":"        from headroom.proxy.helpers import get_memory_injection_mode, log_memory_injection\n        from headroom.proxy.memory_decision import MemoryDecision\n        from headroom.proxy.memory_query import MemoryQuery\n\n        memory_decision = MemoryDecision.decide(\n            headers=request.headers,\n            memory_handler=self.memory_handler,\n            memory_user_id=memory_user_id,\n            mode_name=get_memory_injection_mode(),\n        )\n        memory_decision.apply_to_tags(tags)\n\n        # Rate limiting (use Gemini API key)\n        if self.rate_limiter:\n            rate_key = headers.get(\"x-goog-api-key\", \"default\")[:20]\n            allowed, wait_seconds = await self.rate_limiter.check_request(rate_key)\n            if not allowed:\n                await self.metrics.record_rate_limited(provider=provider_name)\n                raise HTTPException(\n                    status_code=429,\n                    detail=f\"Rate limited. Retry after {wait_seconds:.1f}s\",\n                )\n\n        # Convert Gemini format to messages for optimization\n        system_instruction = body.get(\"systemInstruction\")\n        messages, preserved_indices = self._gemini_contents_to_messages(\n            contents, system_instruction\n        )\n\n        # Store original content entries that have non-text parts before compression\n        preserved_contents = {idx: contents[idx] for idx in preserved_indices}\n\n        # Early exit if ALL content has non-text parts (nothing to compress)\n        if len(preserved_indices) == len(contents):\n            # All content has non-text parts, skip compression entirely\n            # Just forward the request as-is\n            query_params = dict(request.query_params)","sourceCodeStart":380,"sourceCodeEnd":416,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/proxy/handlers/gemini.py#L380-L416","documentation":"The Gemini handler applies its own rate limit keyed by the inbound x-goog-api-key header (first 20 chars) before converting the request. On exhaustion it records a rate_limited metric and raises HTTPException 429 with detail 'Rate limited. Retry after Ns'. Unlike the Anthropic handler it does not set a Retry-After header and does not hold a pre-upstream semaphore at this point, so the parse must come from the detail message.","triggerScenarios":"Sending more Gemini-format requests (x-goog-api-key auth) within the window than the limiter allows for that key prefix; multiple clients sharing one Google API key through the proxy.","commonSituations":"Several agents or team members configured with the same Gemini key funneled through one proxy; a client retry loop that ignores 429; per-key limit set lower than the aggregate fan-out.","solutions":["Parse the wait from the 429 detail text and back off at least that long before retrying.","Raise the limiter's allowance for Gemini keys, or distribute clients across multiple keys so buckets don't collide.","Add client-side request pacing/concurrency limits sized to the configured window."],"exampleFix":"# before\nfor attempt in range(5):\n    resp = post_gemini(body)  # immediate retries keep getting 429\n\n# after\nresp = post_gemini(body)\nif resp.status_code == 429:\n    wait = float(re.search(r\"after ([0-9.]+)s\", resp.json()[\"detail\"]).group(1))\n    time.sleep(wait + 0.5)","handlingStrategy":"retry","validationCode":"# No pre-check available; pace requests to the configured per-key window before sending.","typeGuard":null,"tryCatchPattern":"resp = await client.post(gemini_url, json=body)\nif resp.status_code == 429:\n    m = re.search(r\"after ([0-9.]+)s\", resp.json()[\"detail\"])\n    await asyncio.sleep(float(m.group(1)) + 0.5 if m else 5.0)\n    resp = await client.post(gemini_url, json=body)","preventionTips":["Don't share one x-goog-api-key across many automated clients through the proxy.","Implement exponential backoff that parses the wait from the 429 detail (no Retry-After header on this path).","Keep aggregate request rate under the limiter's per-key window during batch jobs."],"tags":["rate-limit","http-429","gemini","proxy","api-key"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}