Facebook API Rate Limits: Three Systems, One Error
Meta counts your calls in three parallel systems, and the token you send decides which one applies. Maps all three against Graph API v23.0, both usage headers, and the throttle codes Meta splits across two separate pages - including code 9 and subcode 1390008, which appear on neither. Plus 75/90.
Facebook API rate limits are not one limit. Meta runs three parallel systems (Platform Rate Limits, Business Use Case (BUC) limits, and Ad Account limits) and which one governs a request depends on the token you send, not the endpoint you call. That is why the same (#4) Application request limit reached symptom has three different fixes. This guide maps all three, both usage headers, and the throttle codes Meta splits across two separate pages.

Three limit systems, not one
Every formula below was checked against Meta's live rate-limiting reference on 4 August 2026, on Graph API v23.0, the version we run.
| System | Counts | Window | Formula in the docs | Header | Code |
|---|---|---|---|---|---|
| Platform - app token | Your app's calls | 1 h | 200 × Number of Users | X-App-Usage | 4 |
| Platform - user token | That user, across every app | 1 h | Not disclosed | X-App-Usage | 17 |
| BUC - Pages | Per app, per Page | 24 h | 4800 × Number of Engaged Users | X-Business-Use-Case-Usage | 80001 |
| BUC - Instagram / Threads | Per app + app-user | 24 h | 4800 × Number of Impressions | X-Business-Use-Case-Usage | 80002 |
| BUC - Ads Management | Per ad account | 1 h | 300 + 40 × Active ads; Advanced 100000 + 40 × Active ads | X-Business-Use-Case-Usage | 80004 |
| Ad Account | v3.3-and-older Ads API | reset_time_duration | acc_id_util_pct | X-Ad-Account-Usage | 17:2446079 |
Meta settles the overlap in one sentence: "If both Platform and Business Use Case rate limits can be applied to a request, BUC rate limits will be applied."
Two corrections. BUC quota is per product and per asset, not per ad spend or verification status: pages, instagram, ads_insights, ads_management, leadgen, messenger and custom_audience each carry their own budget, and the header's type names the one you are burning. The instagram bucket runs on a different formula and carries a publishing cap on top of the call budget; that side is in Instagram API rate limits. And the Users in 200 × Number of Users are daily active users: 100 engaged users buys 20,000 calls an hour, 100,000 dormant signups buys nothing.
Disclosure: we run the Facebook Graph API, not the Marketing API. The two Ads rows are read from Meta's docs, not from our traffic.
Which token you use decides which limit applies
| Token type | System | Window | Code |
|---|---|---|---|
| App access token | Platform | 1 h | 4 |
| User access token | Platform | 1 h | 17, or 32 on Pages API |
| Page access token | BUC | 24 h | 80001 |
| System user access token | BUC | 24 h | 80001 |
The same GET /{page-id}/ratings call lands in a different bucket depending on which token signs it. With a user token it counts against a rolling one-hour Platform budget and fails with 32. With a Facebook Page access token it counts against a 24-hour BUC budget sized by engaged users and fails with 80001. Same URL, different quota, different error, different recovery time.
That is why Meta recommends system user tokens for high-volume Page reads. The calls are not cheaper; a per-asset budget means one noisy customer cannot drain the bucket everyone else shares.

Reading the headers: percentages, not remaining calls
The two headers are different shapes, not synonyms.
x-app-usage: {"call_count": 28, "total_time": 25, "total_cputime": 25}
x-business-use-case-usage: { "10153848260347724": [ { "type": "pages", "call_count": 97, "total_cputime": 23, "total_time": 23, "estimated_time_to_regain_access": 0 } ] }
X-App-Usage is a flat object with three numbers. X-Business-Use-Case-Usage is a map of business-object-id to an array of objects, one per BUC type, and Meta states it can return up to 32 objects in one response. A parser written for the first will not survive the second.
| Field | What the number is | Throttles at |
|---|---|---|
call_count | Percentage of allowed calls used | 100 |
total_cputime | Percentage of CPU time allotted | 100 |
total_time | Percentage of total time allotted | 100 |
All three are percentages of a budget Meta never discloses, not counts of calls remaining. One operational consequence: you can sit at call_count: 12 and still get throttled, because total_cputime hit 100 on a handful of expensive insights queries. Steer on the maximum of the three, never on call_count alone.
One caveat we owe you: do not assume which header arrives where. Our recommendation importer parses only x-app-usage on /{page-id}/ratings, a path that should sit under BUC with a Page token. If the response carries the BUC header alone, that parser returns null and the throttle never fires. Treat a missing header as no signal, not as safe.
The throttle codes, and where Meta hides them
Verified 4 August 2026 against both Meta pages and our own classifier in retryability.util.ts.
| Code | System | Meaning | What to do | Documented |
|---|---|---|---|---|
4 | Platform, app | App hit its hourly count | Stop app-wide, not per job | Both pages |
9 | Rate limit | Throttled | Transient | Neither |
17 | Platform, user | That user hit their count | Back off per user | Both pages |
17:2446079 | Ad Account | Old Ads API token limit | Back off per ad account | Limits page |
32 | Pages, Platform token | Page limit hit with a user or app token | Move the path to a Page token | Limits page |
341 | Application limit | Downtime or throttling | Back off; transient | Errors guide only |
613 | Custom | Product-specific custom limit | Check that product's docs | Limits page |
613:1996 | Custom | Inconsistent volume from your app | Check your last deploy | Limits page |
80001 | BUC Pages | Too many calls to this Page | Pause, resume from a cursor | Limits page |
80002 | BUC Instagram | Instagram quota exhausted | Same | Limits page |
80004:2446079 | BUC Ads Management | Quota per ad account | Same, per account | Limits page |
sub 1390008 | Posting rate limit | Publish throttled | Transient | Neither |
No single Meta page carries this list: 341 is in the error-handling guide only, 32, 613 and the 80001-80014 family only on the rate-limiting page, and just 4 and 17 on both.
Two rows are ours, not Meta's. Code 9 and subcode 1390008 appear in neither document, nor anywhere in our mirror of the Graph API docs. We classify both as transient because they arrived in production traffic, not because anyone documented them.
Two field notes. Do not key on HTTP status: our import processor catches status === 429 || error.code === 80001 because the Graph code is reliable and the status is not. And Meta's rate-limiting reference never mentions Retry-After, in any section; our adapter records it if it arrives, but nothing promises it. Guides telling you to sleep for Retry-After seconds describe a different API. Wider taxonomy: social media API error handling.
bundle.social
Three Facebook quota systems behind one rate-limit layer.
Three upload hosts and two auth header words, handled behind one endpoint.

estimated_time_to_regain_access and what 0 means
This field arrives only in X-Business-Use-Case-Usage, in minutes. Meta's X-App-Usage header-contents table lists exactly three keys and this is not one of them, so read it if present, never require it.
The trap is the zero. Meta's own sample shows estimated_time_to_regain_access: 0 on entries sitting at call_count: 95 and 97, and 19 on the entry that is actually blocked. 0 means you are not currently blocked. It does not mean retry now. A backoff reading 0 as a green light will spin at 97% quota, and every failed call keeps the count climbing.
Steering on headers before you get blocked
These thresholds are our operational decision, not Meta's numbers. Meta only tells you that 100 throttles.
// apps/api/src/bull-mq/queue.consts.ts APP_USAGE_THROTTLE_THRESHOLD: 75, // soft: slow down APP_USAGE_THROTTLE_DELAY_MS: 30_000, APP_USAGE_CRITICAL_THRESHOLD: 90, // hard: pause and checkpoint
const peak = Math.max(usage.callCount, usage.totalTime, usage.totalCputime); if (peak >= APP_USAGE_CRITICAL_THRESHOLD) { await markRateLimited({ paginationCursor: cursor, rateLimitResetAt: inOneMinute }); return; // exit the job, do not sleep holding a worker slot } await wait(peak >= APP_USAGE_THROTTLE_THRESHOLD ? APP_USAGE_THROTTLE_DELAY_MS : jitteredBaseDelay);
Pick your own numbers. What matters is the Math.max across all three metrics, and that the hard threshold persists a cursor and exits instead of sleeping in-process.
There is a reason we steer rather than retry. Facebook's video publish uploads bytes and creates the post in one non-idempotent step with no idempotency key, so it runs with maximumAttempts: 1 and the first failure is terminal. Every guide on this phrase sells exponential backoff. Backoff is right for reads. On a non-idempotent publish path a rate limit is something you get ahead of, not recover from, which is what the brake on read traffic protects.
Two ways to burn quota without touching your code
Batching does not save quota. Meta's FAQ is blunt: GET /photos?ids=4,5,6 counts as three API calls, identical to three separate requests. Batches cap at 50, and "each call within the batch is counted separately". You save round trips, not budget, so a capacity plan built on batching your way under the limit is planning against a number that does not move.
A deprecated insights metric turns one call into eight. We request eight post metrics in a single /{post-id}/insights?metric=... call. When Meta deprecates one, the whole request fails and our fallback re-requests each metric individually, in parallel: one call becomes eight, at whatever refresh concurrency is running. Nothing errors, the data comes back, the quota is just gone. Every platform has a version of this, see social media API rate limits.
One rate-limit layer instead of fifteen
Everything above is Facebook alone: three quota systems, two header shapes, twelve throttle codes across two documentation pages, two of them undocumented. TikTok, LinkedIn, X and Pinterest each have their own scheme, headers and opinions about what a retry means.
The Facebook API integration in bundle.social absorbs that layer. We parse the usage headers, hold the 75/90 brake, classify every Meta code as retryable or terminal, and route non-idempotent publishes so a throttle cannot become a duplicate post. One error contract across fifteen platforms instead of fifteen taxonomies.
This is not for you if you are building against the Marketing API: we do not run ad-account traffic, and the Ads quota systems above are documentation, not experience. If you publish to Pages and read Page data at volume, it is the layer we needed and could not buy.
Frequently asked questions
What does Facebook error code 4 mean?
(#4) Application request limit reached means your app hit its Platform Rate Limit, a rolling one-hour budget of 200 × Number of Users, counted on daily active users. It is app-wide, so throttling one job will not clear it. Cut call volume across every process using that app token.
How long does a Facebook API rate limit last?
It depends which system you hit. Platform limits use a rolling one-hour window, so pressure eases as old calls age out. BUC Pages limits use a rolling 24-hour window, considerably longer. Meta publishes no fixed reset time; the only estimate is estimated_time_to_regain_access, in minutes.
What is the difference between X-App-Usage and X-Business-Use-Case-Usage?
Different systems, different shapes. X-App-Usage reports Platform usage as a flat object with call_count, total_cputime and total_time. X-Business-Use-Case-Usage reports BUC usage as a map of business-object-id to arrays of objects, up to 32 per response, each with a type. Parse both.
Why do I get error 80001 instead of 32?
Your token. Pages API calls made with a Page or system user access token fall under Business Use Case limits and fail with 80001. The identical call made with a user or app access token falls under Platform Rate Limits and fails with 32. Same endpoint, different quota, different recovery window.
Does Facebook send a Retry-After header on rate limit errors?
Meta's rate-limiting documentation never mentions Retry-After, not for 4, 17, 32 or 80001. Do not build a backoff on it. The documented signals are the usage headers before you are blocked, and estimated_time_to_regain_access after. Record it if it arrives, but treat its absence as normal.