LangGraph Agent Hits Rate Limit After Recursive Loop — Fix Without Losing State
The Exact Error
OpenAI RateLimitError: Rate limit reached for gpt-4o in organization org-xxx on tokens per min (TPM): Limit 30000, Used 29847, Requested 2400.
at APIError.generate (node_modules/openai/error.js:44)
at OpenAI.makeStatusError (node_modules/openai/core.js:395)
Agent trace: step_1 → step_2 → step_3 → step_2 → step_3 → step_2 → ... (47 iterations)
Estimated cost before rate limit: $8.40What's Happening
Your LangGraph agent starts executing normally then enters an infinite loop, hitting the OpenAI rate limit after 47+ recursive steps and generating an unexpected API bill.
Root Cause
LangGraph's conditional edge routing can create circular paths when the agent's state doesn't have a clear termination condition. The graph re-enters the same nodes indefinitely because no budget or step-count circuit breaker is in place at the execution layer.
Quick Diagnosis Checklist
Run through these steps to confirm this is your issue:
Check your graph's conditional edges for circular routing logic
Print agent trace IDs — if the same node appears 3+ times, you're in a loop
Review your termination condition in the should_continue function
Check if your tool-calling node is returning to the agent node without a stop condition
The Permanent Fix
Implement a circuit breaker at the middleware layer that tracks trace IDs across recursive steps and kills execution if cost exceeds a configurable threshold — before the rate limit error triggers.
Drop-In Package That Permanently Fixes This
Instead of building the fix from scratch, use this production-ready package.
// circuit-breaker.middleware.ts
import { CircuitBreaker } from './circuit-breaker.config'
export const config = { matcher: '/api/agent/:path*' }
export default CircuitBreaker.middleware({
budgetUSD: 5.00, // kill after $5 per session
windowMinutes: 60, // rolling window
tracingHeader: 'x-agent-trace-id',
onTripped: async (ctx) => {
await ctx.log({ reason: 'budget_exceeded', cost: ctx.totalCost })
return ctx.kill() // returns 429 + kills execution
}
})