Notes
API throttling, 429s and backoff in an ERP integration
Throttling is an API refusing calls because the caller went over a rate, cost or concurrency limit. The refused call was not executed, so the integration has to keep it, wait and send it again; the waiting scheme is called backoff. Not every system says so with a 429: Shopify GraphQL reports a throttle inside a 200 OK and a NetSuite RESTlet returns a 400.
Updated
What a throttled call looks like
A rate limit is the rule: 100 cost points per second, 5 requests in flight, 500 requests a minute. Throttling is what the API does when a caller breaks the rule: it refuses the call and does not execute it. HTTP has a status for this, 429 Too Many Requests from RFC 6585, and a header that says how long to wait, Retry-After. Several of the systems an ERP integration talks to use only one of the two, or neither.
The responses below were read from each vendor's documentation on October 3, 2026.
| API | The refusal | How long to wait |
|---|---|---|
| Shopify GraphQL Admin API | 200 OK with error code THROTTLED in the body |
No time given. The body reports currentlyAvailable and restoreRate |
| Shopify REST Admin API | 429 |
Retry-After, in seconds |
| NetSuite RESTlet | 400 Bad Request with SSS_REQUEST_LIMIT_EXCEEDED |
Not given |
| NetSuite REST web services | 429 with CONCURRENCY_LIMIT_EXCEEDED |
Not given |
| NetSuite SOAP web services | SOAP fault WS_REQUEST_BLOCKED |
Not given |
| Business Central | 429 over 6,000 requests in a 5-minute sliding window per user, or over 100 open requests |
Until the window has room |
| BigCommerce | 429 |
X-Rate-Limit-Time-Reset-Ms, in milliseconds |
| QuickBooks Online | 429 over 500 requests a minute per company |
60 seconds, per Intuit |
A refusal from BigCommerce on a Standard or Plus store looks like this:
HTTP/1.1 429 Too Many Requests
X-Rate-Limit-Time-Window-Ms: 30000
X-Rate-Limit-Time-Reset-Ms: 15000
X-Rate-Limit-Requests-Quota: 150
X-Rate-Limit-Requests-Left: 0
The quota is 150 requests per window of 30,000 milliseconds, none are left, and the window resets in 15,000 milliseconds. The integration waits 15 seconds.
Four things in this table go wrong in practice.
A throttle inside a success. The Shopify GraphQL Admin API answers a throttled mutation with 200 OK. An integration that checks only the HTTP status records the stock update as delivered, and it was never applied.
A throttle that looks like bad data. A NetSuite RESTlet over the concurrency limit returns 400, the same status as an order with an inactive item. The retry rule has to read the error code: SSS_REQUEST_LIMIT_EXCEEDED is sent again, a validation error is not.
A 429 without headers. BigCommerce documents that a 429 without the rate limit headers means the platform is under high load for that store. There is no reset time to read, so the integration falls back to its own backoff schedule.
A limit shared with others. BigCommerce gives one quota to all apps on a store together. NetSuite counts concurrency per account, across every integration. Shopify counts per app per store. On the first two, counting your own calls does not tell you how much room is left. The numbers per system are in Shopify API rate limits and NetSuite concurrency and governance limits.
Which failures get a retry
A retry only helps when the same call can succeed later. The question behind every failure is whether the call was executed.
| Response | Was the call executed? | What the integration does |
|---|---|---|
Throttle: 429, THROTTLED, SSS_REQUEST_LIMIT_EXCEEDED |
No | Waits, sends it again |
502, 503 |
No | Retries with backoff |
| Timeout, connection dropped | Unknown | Retries only with a duplicate check |
400 for a missing field or inactive item |
No | No retry. To a review list with the reason |
401 |
No | Stops the flow and alerts |
SSS_USAGE_LIMIT_EXCEEDED |
Partly | Makes the batch smaller |
The timeout row is the one that produces duplicates. QuickBooks Online cuts off any request that runs longer than 120 seconds, and Business Central cancels an OData request after 8 minutes. Oracle's documentation states it for NetSuite: when the server has received the request it processes the operation, even if the connection breaks afterwards and the client gets no response. The order may exist. Sending it again without a check creates a second one.
The last row is a limit of a different kind. A NetSuite RESTlet has 5,000 usage units per call. A batch that needs more stops partway with NetSuite's usage limit error. The same batch fails at the same record every time, so waiting changes nothing.
How long to wait
Use what the API says. RFC 9110 defines Retry-After as a number of seconds or an HTTP date. Shopify's REST Admin API sends Retry-After: 2.0, and a request sent before those 2 seconds are over is throttled again. BigCommerce puts the wait in its own header, in milliseconds.
A fixed window resets on its own schedule. BigCommerce refreshes its quota every 30 seconds. Exact Online, an ERP from the Netherlands, allows 5,000 calls per company per day and rejects every further call until the start of the following day. Retrying inside the window spends calls and gains nothing.
When nothing is stated, the pause grows. NetSuite gives no wait time. Oracle's guidance is to send the first retry after a delay and to increase the delay for each subsequent attempt. A common schedule doubles the pause and caps it: 1, 2, 4, 8, 16 and 32 seconds, then 60 and 60. Eight retries add up to 1 + 2 + 4 + 8 + 16 + 32 + 60 + 60 = 183 seconds. A fixed retry every second would be 183 calls over the same period, against a system that just said it is too busy.
A random part in every pause. Oracle warns against creating artificial concurrency peaks by synchronizing retries across threads or applications. Ten workers that were refused together and all wait exactly 4 seconds arrive together again. With a pause drawn at random between half the step and the whole step, the third retry lands somewhere between 2 and 4 seconds and the workers spread out.
A cap on attempts. After the last retry the message goes to a dead-letter queue with its original payload. That part is described in what happens when a sync fails.
Twelve order writes against a limit of five
Backoff handles a refusal after it happened. Pacing prevents the refusal. Take a NetSuite account on the Standard tier, with 5 concurrent requests, and 12 orders that arrive in the same second. Assume each order write takes 2 seconds.
Sent all at once with backoff of 1, 2 and 4 seconds:
- Second 0: 5 writes are accepted and run until second 2. 7 are rejected.
- Second 1: the 7 retry. The first 5 are still running. 7 rejected again.
- Second 3: the 7 retry. 5 are accepted and run until second 5. 2 are rejected.
- Second 7: the last 2 are accepted and finish at second 9.
That is 12 + 7 + 7 + 2 = 28 calls, 16 of them rejected, and 9 seconds until the last order is in NetSuite.
Paced at 4 writes at a time, the same 12 orders go in three groups: seconds 0 to 2, 2 to 4 and 4 to 6. That is 12 calls, no rejection, and 6 seconds. The fifth slot stays free for another integration on the same account.
Pacing needs the remaining budget, and most APIs report it in every response. Shopify GraphQL gives throttleStatus.currentlyAvailable, Shopify REST gives X-Shopify-Shop-Api-Call-Limit: 32/40, BigCommerce gives X-Rate-Limit-Requests-Left. NetSuite reports nothing per response, so the integration counts its own requests in flight against the account limit. How these limits turn into the duration of a full run is worked out in integration throughput.
A retry must create one order
A retry after a timeout sends a call that may already have been executed. The call has to be built so that executing it twice gives the same result as executing it once.
NetSuite's answer is the external ID. In REST web services a PUT on an external ID is an upsert: it creates the record when it does not exist and updates it when it does.
PUT /services/rest/record/v1/salesOrder/eid:SHOP-31877 HTTP/1.1
Content-Type: application/json
Both attempts for store order 31877 address the same record, so the second attempt cannot produce a second sales order. Oracle recommends this approach in its reliability notes for web services: external IDs and upsert operations, to prevent duplicate records.
Shopify uses a key in the request. Mutations that are not repeatable by nature take an idempotency key, a unique string the app generates. A repeated request with the same key and the same parameters is executed once, however many times it is sent.
Where an API offers neither, the integration looks the order up by the store order number before it creates anything. That lookup, and the customer side of the same problem, is covered in duplicate customers and duplicate orders.
The trade between throughput and reliability
Standard connectors and iPaaS platforms document one answer to THROTTLED errors: a smaller page size and a longer schedule interval. The errors stop, and the data in the store gets older.
Take an item with 4 pieces left that sells 6 an hour through the ERP's other channels. It is sold out after 40 minutes. On a 15-minute schedule the store learns that at most 15 minutes late. On a 60-minute schedule with a run at the start of the hour, the store shows 4 pieces until the next run, 20 minutes after the last one is gone, and every order in those 20 minutes is an oversell.
The interval is one setting for the whole flow. A queue makes the same trade per call:
- It reads the remaining budget from each response and slows down before the limit, not after.
- It lowers the cost of the expensive call, for example a smaller page on the one query that asks for nested data, and leaves the cheap calls alone.
- It gives orders priority over a catalog refresh when both compete for the same budget.
- It keeps the schedule, so a throttle delays one call by seconds and does not make every run an hour apart.
What this means for your integration
We build every integration with a queue between the systems. Calls go out at the pace the API allows, a throttle leads to a wait and a new attempt, and every write carries an external ID or a duplicate check, so a retry creates one order. What still fails after the last attempt stays in the dashboard with its original payload and can be sent again. We host and run the integration, so a limit that a vendor changes is ours to follow and adjust for.
The limits of your systems and your order count on a peak day decide how the flows share the budget. The flows themselves are on integrations, and the order flow with its fields on orders, customers and shipping. Tell us which systems you run and how many orders a peak hour brings, and we work out which limits apply and what they mean for a peak hour.
Frequently asked questions
What does HTTP 429 Too Many Requests mean?
The API understood the request and refused it because the caller sent too many requests in a given time. The status is defined in RFC 6585. The response may carry a Retry-After header with the time to wait. The request was not executed, so it has to be sent again after the wait.
Does every API return a 429 when it throttles?
No. The Shopify GraphQL Admin API returns 200 OK with the error code THROTTLED in the body. A NetSuite RESTlet over the concurrency limit returns 400 Bad Request with SSS_REQUEST_LIMIT_EXCEEDED, and NetSuite SOAP web services return a SOAP fault. An integration has to read the error code in the body, not only the HTTP status.
What is exponential backoff?
It is a retry schedule in which each pause is twice the previous one, up to a cap: 1 second, then 2, 4, 8 and so on. The API gets time to recover and the number of repeated calls stays small. Eight retries on a schedule capped at 60 seconds span 183 seconds. A random part in each pause keeps parallel workers from retrying at the same instant.
Are orders lost when an API throttles?
Not when the integration has a queue. The order stays in the queue, the integration waits for the time the API indicates and sends it again. An integration that calls the API directly and ignores the refusal skips the order without anyone seeing it. What happens after the last failed attempt is described in what happens when a sync fails.
How many times should an integration retry?
There is no standard number. It follows from how long the failure can last: a throttle clears in seconds, an outage can take hours. cXML, the protocol behind PunchOut, recommends 10 retries one hour apart for a transient error. Whatever the number, the last failed attempt moves the message to a dead-letter queue instead of dropping it.
Can I get a higher limit instead?
Often, on a higher plan. Shopify Plus restores 1,000 GraphQL cost points per second where a standard plan restores 100. NetSuite adds 10 concurrent requests per SuiteCloud Plus license. BigCommerce gives a Pro store 450 requests per 30 seconds where Standard and Plus stores get 150. The limit is higher and still exists, so the integration has to handle the refusal either way.
Tell us what you need
Tell us which systems you use and what needs to work better. We reply personally within one working day. We help you define the work, then agree the price and timing before you commit.
- You know the price and the timeline before you commit to anything
- We work out the connection that takes the work off your hands
- We handle the build, hosting, monitoring and maintenance
- Your team has less information to retype and fewer discrepancies to chase