Meta Ads Rate Limits: How to Handle Them
A practical guide to Meta Ads API rate limits. Covers rate limit tiers, detecting 429 errors, implementing backoff strategies, and how Xylo's caching layer helps.
Why Rate Limits Exist
Meta's Marketing API serves millions of applications making billions of requests. Rate limits prevent any single application from monopolizing resources and degrading the experience for others. They also protect Meta's backend systems from unintentional abuse -- a runaway loop in your code should not be able to DDoS Meta's infrastructure.
Understanding how these limits work is essential for building reliable ad integrations. An application that ignores rate limits will experience intermittent failures, dropped data, and eventually temporary blocks.
The Business Use Case Rate Limit System
Meta uses a system called Business Use Case Rate Limits (BUC Rate Limits) for the Marketing API. Unlike a simple "X requests per minute" model, BUC rate limits consider:
- Your app tier. Standard apps get higher limits than development-tier apps.
- The endpoint type. Read endpoints have different limits than write endpoints.
- The ad account. Limits are partially per-account, so one account hitting its limit does not affect another.
- A usage percentage. Meta tracks your usage as a percentage of your allocated capacity.
Reading the Usage Header
Every Marketing API response includes a usage header:
x-business-use-case-usage: {
"act_123456789": [
{
"type": "ads_management",
"call_count": 28,
"total_cputime": 15,
"total_time": 24,
"estimated_time_to_regain_access": 0
}
]
}
The key fields:
| Field | Meaning | Threshold |
|---|---|---|
call_count |
Percentage of call volume used | Throttled at 100 |
total_cputime |
Percentage of CPU time used | Throttled at 100 |
total_time |
Percentage of total processing time used | Throttled at 100 |
estimated_time_to_regain_access |
Minutes until you regain access | 0 = not blocked |
When any of the three percentage metrics hits 100, your requests will be throttled. The estimated_time_to_regain_access field tells you how long to wait.
Rate Limit Tiers
Your rate limit capacity depends on your app's development tier:
| Tier | Call Volume | CPU Time | Who Gets It |
|---|---|---|---|
| Development | Low | Low | New apps, testing |
| Standard | Medium | Medium | Approved apps in production |
| Advanced | High | High | Large-scale apps with Meta partnership |
To move from Development to Standard tier, your app needs Meta's app review approval with the ads_management permission.
What Happens When You Hit the Limit
When your usage exceeds the threshold, Meta returns a 400 error (not 429, despite being a rate limit):
{
"error": {
"message": "(#32) Application request limit reached",
"type": "OAuthException",
"code": 32,
"fbtrace_id": "AbCdEf123GhI"
}
}
Error code 32 is the primary rate limit error. You may also see:
| Error Code | Subcode | Meaning |
|---|---|---|
| 32 | -- | General rate limit |
| 4 | -- | Application-level rate limit |
| 17 | -- | User-level rate limit |
| 32 | 2446079 | Ads API rate limit specifically |
| 80004 | -- | Ad account rate limit |
Implementing Retry Logic
Exponential Backoff
The standard approach is exponential backoff with jitter:
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 3
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.ok) {
return response;
}
const body = await response.json();
const isRateLimited =
body.error?.code === 32 ||
body.error?.code === 4 ||
body.error?.code === 17 ||
body.error?.code === 80004;
if (!isRateLimited || attempt === maxRetries) {
throw new Error(`Meta API error: ${body.error?.message}`);
}
// Exponential backoff with jitter
const baseDelay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
const jitter = Math.random() * 1000;
const delay = baseDelay + jitter;
console.log(
`Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +
`Retrying in ${Math.round(delay)}ms...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error("Max retries exceeded");
}
Using the Usage Header
For more intelligent throttling, read the usage header proactively:
function parseUsageHeader(response: Response, accountId: string) {
const header = response.headers.get("x-business-use-case-usage");
if (!header) return null;
const usage = JSON.parse(header);
const accountUsage = usage[accountId]?.[0];
if (!accountUsage) return null;
return {
callCount: accountUsage.call_count,
cpuTime: accountUsage.total_cputime,
totalTime: accountUsage.total_time,
minutesToRegain: accountUsage.estimated_time_to_regain_access,
};
}
async function fetchWithThrottling(url: string, options: RequestInit, accountId: string) {
const response = await fetch(url, options);
const usage = parseUsageHeader(response, accountId);
if (usage) {
// If approaching the limit, slow down
if (usage.callCount > 75 || usage.cpuTime > 75 || usage.totalTime > 75) {
console.warn(`Approaching rate limit: ${JSON.stringify(usage)}`);
await new Promise((resolve) => setTimeout(resolve, 5000));
}
// If blocked, wait the specified time
if (usage.minutesToRegain > 0) {
const waitMs = usage.minutesToRegain * 60 * 1000;
console.warn(`Rate limited. Waiting ${usage.minutesToRegain} minutes.`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
}
}
return response;
}
Request Batching
Meta supports batch requests that combine multiple API calls into a single HTTP request. This counts as one call against your rate limit:
curl -X POST "https://graph.facebook.com/v22.0/" \
-d 'batch=[
{"method":"GET","relative_url":"act_123/campaigns?fields=id,name,status&limit=10"},
{"method":"GET","relative_url":"act_123/adsets?fields=id,name,status&limit=10"}
]' \
-d "access_token=EAABsbCS1..."
Batch requests can include up to 50 operations. This is particularly useful for read-heavy operations like pulling data for multiple campaigns.
How Xylo Handles Rate Limits
Building all of this retry logic, usage tracking, and batching is significant engineering work. Xylo handles it for you at three levels:
1. Response Caching
Xylo caches read responses with resource-appropriate TTLs:
| Resource | Cache TTL | Effect |
|---|---|---|
| Campaign lists | 5 minutes | A dashboard polling every 30 seconds makes 1 upstream call per 5 minutes |
| Performance insights | 15 minutes | Reporting queries hit Meta at most 4 times per hour |
| Targeting options | 1 hour | Audience targeting data refreshes infrequently |
| Ad creatives | 10 minutes | Creative data changes less often than metrics |
Every Xylo response includes cache metadata:
{
"meta": {
"cached": true,
"cache_age_seconds": 127,
"meta_api_version": "v22.0"
}
}
2. Automatic Retry
When Xylo's upstream call to Meta hits a rate limit, Xylo retries with exponential backoff (up to 3 attempts) before returning an error. Your application does not need any retry logic for transient rate limits.
3. Structured Error Responses
If the rate limit persists after retries, Xylo returns a clear error:
{
"error": {
"code": "RATE_LIMITED",
"message": "Meta API rate limit exceeded. Retry after 300 seconds.",
"meta_error": {
"code": 32,
"subcode": 2446079
}
}
}
Your application can handle this single error code instead of checking for multiple Meta error codes and parsing usage headers.
Best Practices for Avoiding Rate Limits
-
Request only the fields you need. Meta's API supports field selection. Requesting fewer fields uses less CPU time on Meta's side, reducing your
total_cputimeusage. -
Use date presets instead of custom ranges. Date presets like
last_7dare optimized on Meta's backend. Custom date ranges require more processing. -
Paginate efficiently. Request the maximum page size (250 for most endpoints) to minimize the number of requests per data set.
-
Cache aggressively on your side. If you are building a dashboard, cache responses in your application layer. Stale ad data that is 5 minutes old is perfectly fine for most use cases.
-
Batch read requests. If you need data from multiple campaigns, use batch requests instead of individual calls.
-
Spread writes over time. If you need to update 50 campaigns, do not fire all 50 requests simultaneously. Space them out over a few seconds.
Monitoring Your Usage
If you are making direct Meta API calls (without Xylo), track your usage over time:
const usageLog: Array<{
timestamp: Date;
accountId: string;
callCount: number;
cpuTime: number;
totalTime: number;
}> = [];
// After each request, log usage
const usage = parseUsageHeader(response, accountId);
if (usage) {
usageLog.push({
timestamp: new Date(),
accountId,
callCount: usage.callCount,
cpuTime: usage.cpuTime,
totalTime: usage.totalTime,
});
}
// Alert when approaching limits
const latestUsage = usageLog[usageLog.length - 1];
if (latestUsage && latestUsage.callCount > 80) {
console.warn(`Rate limit warning: ${latestUsage.callCount}% call volume used`);
}
Getting Started
If you are building a Meta Ads integration and want to skip the rate limit engineering:
- Sign up for Xylo and connect your Meta ad account.
- Generate an API key from the dashboard.
- Make requests to Xylo's API -- caching, retry, and error normalization are handled automatically.
For more on the Meta Ads API, read our developer guide. To understand the broader comparison between raw API access and abstraction layers, see Meta Ads API vs Xylo.
Check the API documentation for complete endpoint details and response formats.
Ready to simplify your ads API integration?
Get started with Xylo in minutes. One API key for every ad platform.