- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
Rate Limits, Quotas and Backoff: Calling Third-Party APIs Without Getting Blocked
About Post
Your integration worked perfectly for months. Then one morning the nightly sync job starts failing with 429 Too Many Requests, your code immediately retries, the retries get rejected too, and by the time anyone looks, you've spent the whole day's quota hammering an API that was politely asking you to slow down.
Every third-party API has limits. Payment gateways, SMS providers, maps, email services, AI models. The question isn't whether you'll hit them, it's whether your code reacts like a calm adult or a toddler pressing the lift button.
Here's how to handle rate limits properly: what the errors mean, how backoff with jitter works, and why a circuit breaker belongs in any integration you depend on.
Rate limits and quotas are not the same thing
They both end in "no", but they need different reactions:
- Rate limit: how fast you may call. "100 requests per minute." Wait a little and you're fine again.
- Quota: how much you may use in a longer period. "10,000 requests per day" or a monthly spend cap. Waiting a few seconds won't help; you're done until the window resets or someone upgrades the plan.
Many APIs return 429 for both. Read the response body and headers to tell them apart, because retrying a quota error every few seconds is just noise in your logs (and sometimes in theirs).
Read what the API is telling you
A well-behaved API tells you exactly when to come back. Look for:
Retry-After: a standard HTTP header, either a number of seconds or an HTTP date. If it's there, respect it. It beats any formula you invent.- Limit headers like
X-RateLimit-RemainingandX-RateLimit-Reset. Names vary between providers, so check their docs. These let you slow down before you get rejected.
Not every error deserves a retry, either. My rough rule:
| Response | Retry? | Why |
|---|---|---|
| Timeout / connection error | Yes, with backoff | Usually temporary (but see the note on idempotency below) |
| 429 Too Many Requests | Yes, after Retry-After | You're too fast, not wrong |
| 502, 503, 504 | Yes, with backoff | The provider or a proxy is struggling |
| 500 | Maybe, a few times | Could be temporary, could be your payload |
| 400, 401, 403, 404, 422 | No | Retrying a wrong request just repeats the mistake |
Exponential backoff, and why jitter matters
Retrying immediately is the worst option. If the provider is overloaded, you add to the load at exactly the wrong moment. Exponential backoff doubles the wait each time: 0.5s, 1s, 2s, 4s, up to a cap.
But backoff alone has a sneaky flaw. Picture a hundred workers that all failed at the same moment. With plain exponential backoff they all wait exactly 1 second, then all retry together, fail together, wait exactly 2 seconds... You've turned your workers into a synchronised swimming team that crashes into the API in waves. This is the "thundering herd".
Jitter fixes it by adding randomness. A popular version, "full jitter", picks a random delay between zero and the exponential value:
function backoffMs(int $attempt, int $baseMs = 500, int $capMs = 30_000): int
{
$ceiling = min($capMs, $baseMs * (2 ** ($attempt - 1)));
return random_int(0, $ceiling); // full jitter
}
Now the hundred workers spread their retries across the window instead of arriving as one wave.
Putting it together with Laravel's HTTP client
Laravel's HTTP client has retry() built in. The delay can be a closure, and a when callback decides which failures are worth retrying:
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
$response = Http::timeout(10)->retry(
times: 5,
sleepMilliseconds: function (int $attempt, Throwable $e) {
$after = $e instanceof RequestException
? $e->response->header('Retry-After') : '';
return is_numeric($after) ? (int) $after * 1000 : backoffMs($attempt);
},
when: fn (Throwable $e) => $e instanceof ConnectionException
|| ($e instanceof RequestException
&& in_array($e->response->status(), [429, 502, 503, 504])),
)->get('https://api.example.com/v1/rates');
This is simplified (it ignores the HTTP-date form of Retry-After), but it covers the important parts: respect the server's hint, back off with jitter otherwise, and only retry errors that can succeed later.
One important caveat: retrying a POST can do the work twice. A timeout doesn't mean the request failed; it might have succeeded and the response got lost. For anything that creates or charges, send an idempotency key if the provider supports one.
Don't sleep inside a web request
Five retries with backoff can add up to a long wait. Inside a web request, that's a user staring at a spinner and a PHP worker doing nothing. Move third-party calls to a queued job where you can, and let the queue do the waiting.
Laravel jobs can release themselves back to the queue with a delay, and the RateLimited job middleware lets you stay under a provider's limit instead of discovering it the hard way:
// AppServiceProvider::boot()
RateLimiter::for('sms-provider', fn (object $job) => Limit::perMinute(60));
// In the job
public function middleware(): array
{
return [new RateLimited('sms-provider')];
}
public function retryUntil(): DateTime
{
return now()->addHours(2);
}
Use the limit from your provider's documentation, set slightly below it. Note that released jobs count as attempts, so use retryUntil() rather than a small $tries for rate-limited jobs.
The circuit breaker: knowing when to stop trying
Backoff is polite to a provider that's briefly busy. But if the provider is properly down for twenty minutes, every request still waits, retries and fails. Your queue fills up, your workers are all busy waiting on a dead API, and unrelated jobs get stuck behind them.
A circuit breaker works like the one in your house. After enough failures in a row, it "trips" and calls fail instantly for a cool-down period. After that, a few calls are let through to test whether the provider is back.
final class CircuitBreaker
{
public function __construct(
private string $name, private int $threshold = 5, private int $coolDown = 60,
) {}
public function isOpen(): bool
{
return Cache::has("cb:{$this->name}:open");
}
public function recordFailure(): void
{
if (Cache::increment("cb:{$this->name}:fails") >= $this->threshold) {
Cache::put("cb:{$this->name}:open", true, $this->coolDown);
Cache::forget("cb:{$this->name}:fails");
}
}
public function recordSuccess(): void
{
Cache::forget("cb:{$this->name}:fails");
}
}
Before calling the provider, check isOpen(). If it's open, release the job for later or show a friendly "this service is temporarily unavailable" message instead of a timeout. It's a deliberately minimal sketch, using a shared cache like Redis so all workers see the same state; packages and gateways offer fuller versions.
The three layers: stay under the limit (throttling), recover from brief failures (backoff with jitter), and stop hammering a dead service (circuit breaker). Most integrations only have the middle one, and only half of it.
A checklist for your next integration
- Know the provider's rate limits and quotas before you write code, not after the first 429.
- Set a timeout on every outgoing request. The default can be far too long.
- Retry only temporary errors, respect
Retry-After, and add jitter. - Use idempotency keys for anything that creates, sends or charges.
- Do slow third-party work in queued jobs, throttled below the provider's limit.
- Add a circuit breaker for services your app can't afford to wait on.
- Log and alert on 429s. A rising number is an early warning that you're outgrowing your plan.
Which API has given you the most creative rate-limit behaviour? I'm convinced every provider invents its own headers just to keep us humble.

Be first to comment it...