Oracle Fusion REST API Rate Limits: Handling 429 Too Many Requests
A nightly sync job that’s worked fine for months starts throwing 429 Too Many Requests partway through, and the failure looks random — same code, same records, no error last week. It isn’t random. Oracle Fusion REST APIs are rate-limited at the identity-domain level, and once an integration’s call volume crosses that line, every request gets throttled until the window resets. The Customer Connect forums have multiple open threads asking what the actual limit is and how to handle the response, and the answer is scattered across a few generic pages — nothing walks through the mechanism and the recovery pattern together. This is that guide.
All examples use an anonymized pod (acme.fa.us2.oraclecloud.com) and placeholder identifiers — swap in your own.
Why 429 happens
Rate limiting on Oracle Fusion REST APIs is enforced at the identity domain (IDCS/IAM) level, not by the individual Fusion module you’re calling. That means a limit isn’t really “the workers API’s limit” or “the invoices API’s limit” — it’s a ceiling on how much total API traffic your identity domain type allows in a given window, shared across every integration authenticating against it.
Two things follow from that:
- The exact number isn’t published as a hard, documented figure. Oracle Customer Connect threads discussing it (“429 Too Many Requests”, “What is Rate limit for REST API”) converge on an unofficial estimate around 5,000 calls per hour per user — treat that as a rough planning number, not a guaranteed threshold, since it varies by identity domain type and can change without a version-notes announcement the way endpoint behavior does.
- It’s shared, not per-endpoint. A batch job hammering
/workersand an interactive user clicking around the same pod at the same time draw from the same bucket. A 429 on a request that looks completely unrelated to your heavy job can be your heavy job’s fault.
Reading the response
A throttled request comes back like this:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Retry-After (seconds) is the signal to respect — it’s Oracle telling you how long to wait before the next attempt has a real chance of succeeding. Not every response includes it consistently, so treat it as authoritative when present and fall back to your own backoff schedule when it’s absent.
The pattern that doesn’t work
# Don't do this — retrying immediately just re-triggers the same throttle
while true; do
curl -s -o /dev/null -w "%{http_code}" \
-u integration.user \
"https://acme.fa.us2.oraclecloud.com/hcmRestApi/resources/11.13.18.05/workers?limit=500&offset=$OFFSET"
done
A tight retry loop with no delay doesn’t recover from a 429 — it extends the throttle window, because every immediate retry counts against the same limit that triggered the 429 in the first place.
Exponential backoff with jitter
The recovery pattern: on a 429, wait, then retry with a growing delay, capped at a maximum, with a small random jitter added so that if multiple workers hit the limit at the same moment, they don’t all retry in lockstep and re-trigger each other.
attempt=0
max_attempts=5
base_delay=2 # seconds
max_delay=60 # seconds
while [ $attempt -lt $max_attempts ]; do
status=$(curl -s -o response.json -w "%{http_code}" \
-u integration.user \
"https://acme.fa.us2.oraclecloud.com/hcmRestApi/resources/11.13.18.05/workers?limit=500&offset=$OFFSET&orderBy=PersonId")
if [ "$status" = "429" ]; then
retry_after=$(curl -s -I -u integration.user \
"https://acme.fa.us2.oraclecloud.com/hcmRestApi/resources/11.13.18.05/workers?limit=1" \
| grep -i '^Retry-After:' | awk '{print $2}' | tr -d '\r')
delay=${retry_after:-$(( base_delay * (2 ** attempt) ))}
delay=$(( delay > max_delay ? max_delay : delay ))
jitter=$(( RANDOM % 3 ))
sleep $(( delay + jitter ))
attempt=$(( attempt + 1 ))
continue
fi
break
done
Same idea as pseudo-code, module-agnostic:
attempt = 0
while attempt < max_attempts:
response = call_api(request)
if response.status == 429:
delay = response.headers.get("Retry-After") or min(base_delay * (2 ** attempt), max_delay)
sleep(delay + random_jitter())
attempt += 1
continue
break
Cap the attempts. A 429 that keeps recurring after several backed-off retries usually means the integration’s total call volume genuinely exceeds what the identity domain allows — more patience won’t fix that, a design change will (see below). Five attempts with a 60-second cap is a reasonable ceiling; looping indefinitely just hides a capacity problem behind a job that never finishes.
When backoff isn’t the real fix
Backoff handles occasional throttling. It doesn’t fix an integration whose steady-state call volume is structurally too high. If a job is 429-ing repeatedly rather than occasionally, the actual fix is usually one of:
- Reduce call count, not just retry harder. A pagination loop at
limit=500over a large resource is already the minimum number of calls for that page size — if it’s still tripping limits, the fix is to run it in a lower-traffic window, not to retry faster. - Use FBDI for bulk loads instead of looped REST POSTs. REST is built for transactional, low-volume operations — an interactive create, a single record update, a targeted query. High-volume data loading (thousands of records) is what File-Based Data Import (FBDI) and the erpintegrations resource exist for; a REST POST loop over 10,000 records will hit rate limits by design, not by accident.
- Separate interactive and batch traffic where possible. If a scheduled sync and interactive users share the same identity domain and integration user, the batch job’s volume eats into the budget users need for normal UI/API interaction. Running heavy syncs during low-traffic windows and keeping batch call volume proportionate reduces contention.
Transactional vs. bulk: know which one you’re building
| Transactional | Bulk | |
|---|---|---|
| Pattern | Single-record create/update, on-demand query | Full extracts, mass loads, scheduled syncs |
| Volume | Low, spread over time | High, concentrated in a run |
| Right tool | REST API | FBDI / HCM Data Loader / erpintegrations |
| 429 risk | Low, occasional — backoff is sufficient | High if forced through REST — redesign, don’t just retry |
A REST integration that started as “sync a handful of records when they change” and grew into “extract the whole table nightly” is the most common way a previously-reliable job starts 429-ing — the call pattern outgrew the tool it was built on.
Common gotchas
- Retrying without any delay. Immediately re-issuing the same request against an active throttle just extends it.
- Ignoring
Retry-Afterwhen it’s present. It’s Oracle’s own signal for how long the throttle lasts — a fixed short backoff that ignores it will keep colliding with the window. - No retry cap. An unbounded retry loop against a structurally-too-high call volume never succeeds — it just burns time until something else times out.
- Treating the ~5,000/hour figure as a hard contract. It’s a widely cited unofficial estimate from community threads, not a documented SLA — plan with margin, don’t build logic that assumes an exact number.
- Not distinguishing 429 from other failures in monitoring. A job that logs every failure identically makes it hard to see “this fails only under load” versus “this is actually broken” — tag 429s distinctly so the pattern is visible in your own metrics.
Where this fits with everything else
Rate limiting is a write-and-read-path concern that sits alongside the other resilience mechanics we’ve covered: it’s independent of ETag/If-Match concurrency (a 429 means you never got far enough to conflict with anyone), and it compounds with pagination — a large paged extraction is exactly the shape of workload most likely to trip a rate limit, which is why the two are worth reading together. For the rest of the request lifecycle — auth, q filters, finders — see the full Oracle Fusion API guide and the endpoint catalog.
This post is part of our complete Oracle Fusion API guide — auth, base URLs, q filters, finders, and key endpoints in one place.