NetSuite concurrency limits, and why the usual retry loop misses them
- NetSuite enforces two separate budgets. Account concurrency caps how many requests run at the same time. Script governance units cap how much work one execution may do. A post that treats them as one “rate limit” gets both wrong.
- Concurrency is pooled account-wide, not per integration. Oracle’s documentation states the account limit “covers the total number of web services and RESTlet requests combined.”
- The pool size is your service tier plus 10 per SuiteCloud Plus license: Standard 5, Premium 15, Enterprise 20, Ultimate 20. Legacy Service Tier 3 accounts get 2.
- A RESTlet rejected for concurrency returns HTTP 400 with
SSS_REQUEST_LIMIT_EXCEEDED— not 429. Retry code that keys on 429 never fires for it, so the request is dropped instead of retried. - One limit produces four different rejection shapes across RESTlet, SOAP request-level credentials, SOAP token-based authentication, and the AI Connector Service. Your retry predicate needs all four.
A NetSuite integration that passed load testing and then failed on its first promotion day usually failed at one of two gates, and the team investigating it usually cannot tell which. NetSuite runs two independent budgets: an account-wide pool of simultaneous requests, and a per-execution allowance of governance units. They have different scopes, different owners, different error codes, and different fixes. Collapsing them into “the NetSuite rate limit” is why capacity plans miss and why retry code silently drops the exact requests it was written to catch.
This guide gives the documented numbers for both budgets, the four rejection shapes a single concurrency limit produces, a retry predicate that actually matches them, and the capacity checklist to run before a traffic spike rather than during one. Every limit below is cited to Oracle’s own documentation and was verified on 11 August 2026.
Contents
- NetSuite enforces two budgets, and only one is a rate limit
- What is my account’s concurrency limit?
- Why retry-on-429 never fires for a RESTlet
- Does the authentication method change the error?
- Where the request is actually rejected
- How do I reserve concurrency for one integration?
- The AI Connector Service draws from the same pool
- What Oracle actually recommends for retries
- A retry predicate that catches all four shapes
- SuiteQL paging is a page-size ceiling, not a second concurrency lane
- When to move from per-record calls to batch work
- Capacity checklist to run before the spike
NetSuite enforces two budgets, and only one is a rate limit
NetSuite applies two governance systems to integration traffic, and they never substitute for each other. Account concurrency limits how many inbound requests may be in flight simultaneously across the entire account. Script governance limits how much work a single execution may perform, measured in usage units. A request can pass the first and die at the second, or be rejected at the first without ever reaching a script.
The distinction decides the fix. Concurrency exhaustion is solved by queueing on the client, staggering schedules, or buying capacity. Governance exhaustion is solved by rewriting the script — fewer units per record, or a map/reduce script instead of a scheduled one. Applying the wrong fix is common: adding a retry loop to a script that died at 5,000 units just burns the same units again. Both budgets sit underneath every pattern in the NetSuite and WooCommerce integration guide hub, which is why they are worth separating once rather than rediscovering per project.
| Property | Account concurrency | Script governance units |
|---|---|---|
| Scope | Whole account, all integrations combined | One script execution |
| Measures | Simultaneous in-flight requests | Work performed, in usage units |
| Typical size | 5 to 20, plus 10 per SuiteCloud Plus license | RESTlet 5,000; scheduled 10,000; user event 1,000 |
| Error on exhaustion | Four shapes — see the matrix below | SSS_USAGE_LIMIT_EXCEEDED |
| Work already done | None — the request never ran | Partial work is committed |
| Correct fix | Client-side queue, stagger, or add licenses | Rewrite the script; map/reduce for unbounded work |
| Safe to retry blindly? | Yes — nothing executed | No — may double-apply committed work |
The two governance budgets NetSuite applies to integration traffic, and why the same retry strategy cannot serve both.
The last row is the one worth internalising. A concurrency rejection is the only failure in this stack that is unambiguously safe to retry, because the request was refused before any script ran. Governance exhaustion and network timeouts are both unknown-outcome failures, and they need the idempotency discipline covered in the WooCommerce to NetSuite sync guide — write an externalId on every order create so a retry collides on uniqueness instead of duplicating the Sales Order.
What is my account’s concurrency limit?
Your concurrency limit is your service tier’s base value plus 10 for each SuiteCloud Plus license, and you can read the live number at Setup > Integration > Integration Governance. Oracle’s documentation states plainly that the account governance limit “covers the total number of web services and RESTlet requests combined” — one pool, shared by every integration, every user, and every scheduled job that calls in.
NetSuite has two tier vocabularies in circulation. Accounts sold from June 2020 use named tiers; older accounts still carry numbered tiers, and the numbering is counter-intuitive — Service Tier 3 is the smallest, not the largest.
| Service tier (current naming) | Base concurrent requests | Legacy tier | Base concurrent requests |
|---|---|---|---|
| Standard | 5 | Shared | 5 |
| Premium | 15 | Service Tier 3 | 2 |
| Enterprise | 20 | Service Tier 2 | 10 |
| Ultimate | 20 | Service Tier 1 and 1+ | 15 |
| Each SuiteCloud Plus license: +10 | Service Tier 0 | 20 | |
Base account concurrency by service tier, per Oracle’s concurrency governance documentation. Production, sandbox and release preview accounts are each governed by their own limit.
Oracle’s worked examples: Service Tier 1 with five SuiteCloud Plus licenses gives 65 concurrent requests (15 + 5 × 10); Ultimate with five licenses gives 70 (20 + 5 × 10); a standard tier with one license gives 15 (5 + 10). Note what the arithmetic implies for a Standard-tier account with no SuiteCloud Plus license: five simultaneous requests, total, for every integration you run. A connector that opens one connection per order saturates that pool at six concurrent orders.
Published guidance frequently reports “15 concurrent requests” as the NetSuite default. That figure is Premium, or legacy Tier 1 — it is three times the Standard-tier allowance, and planning capacity against it is how a Standard-tier account discovers its real limit during a promotion. Read the Integration Governance page rather than a table on the internet, including this one.
Why retry-on-429 never fires for a RESTlet
A RESTlet rejected for exceeding account concurrency returns HTTP 400 Bad Request with the SuiteScript error code SSS_REQUEST_LIMIT_EXCEEDED. It does not return 429. Retry logic written around if (status === 429) — the shape published in most integration guidance, and the shape this article previously carried — evaluates false, falls through to the error branch, and discards a request that NetSuite was explicitly inviting you to send again.
The failure is quiet in the worst way. The integration reports an error rate rather than a throttling event, the retry counter stays at zero, and dashboards show a small number of malformed-request failures. Nothing in that signal points at capacity, so teams investigate payload validation for a week while the real constraint is a pool of five.
| Surface | Authentication | What NetSuite returns on a concurrency rejection |
|---|---|---|
| RESTlet | Any | HTTP 400 Bad Request + SSS_REQUEST_LIMIT_EXCEEDED |
| SOAP web services | Request-level credentials | SOAP fault ExceededRequestLimitFault + WS_CONCUR_SESSION_DISALLWD |
| SOAP web services | Token-based authentication | SOAP fault ExceededConcurrentRequestLimitFault + WS_REQUEST_BLOCKED |
| AI Connector Service | Any | A Too Many Requests error; the AI client must reissue the call |
One account concurrency limit, four rejection shapes. Only the last resembles the 429 that most retry code is written to catch.
Two of these four are not HTTP status codes at all — they are SOAP fault types, readable only after the envelope is parsed. A retry layer that inspects transport status and never reads the fault body cannot see them either. That is the second half of the same defect, and it is why the predicate in the corrected implementation below matches on documented error identifiers rather than on status codes alone.
Does the authentication method change the error?
Yes, and it changes the scope of the limit as well. Oracle documents three distinct behaviours, which is the reason two SOAP rows appear in the matrix above rather than one.
- Token-based authentication and RESTlets: there are no per-user limits for concurrent requests when concurrency governance is enabled. Traffic is capped only by the account maximum.
- SOAP using login/logout operations or request-level credentials: the older per-user governance limits are still maintained, in addition to the account pool.
- Outbound Single Sign-on (SuiteSignOn): concurrency is governed per user and additionally per account — two ceilings applying to the same traffic.
The practical consequence is that migrating an integration’s authentication changes its throughput profile even when no code path changes. Moving a SOAP integration off request-level credentials removes a per-user ceiling; the account pool then becomes the only constraint, which usually raises effective throughput but concentrates contention where every other integration already competes. Plan the migration described in the TBA versus OAuth 2.0 comparison with that shift in mind, and re-measure concurrency afterwards rather than assuming the old headroom carried over.
Where the request is actually rejected
Requests pass two gates in sequence. The concurrency gate is evaluated at the account boundary before any script is loaded; the governance meter runs inside the execution and stops it mid-flight. Knowing which gate produced an error tells you whether any work was committed.
A request meets the account concurrency pool before any script loads, and the per-execution governance meter only after it does.
How do I reserve concurrency for one integration?
You can carve a fixed slice of the account pool for a named integration by setting the Concurrency Limit field on its integration record. Oracle’s stated purpose is twofold: to guarantee a critical integration the bandwidth it needs, and to stop one integration consuming so much of the pool that it starves the others.
The mechanic has a constraint that catches teams out. Allocating concurrency to one application reduces the limit available to every integration that does not have a specific allocation. Oracle sets the minimum unallocated value at one, reserved for integrations without a defined limit and for creating or auto-installing new integration records — and the MAX Concurrency Limit field is always one less than the account’s total unallocated limit.
On a Standard-tier account with a pool of five, allocating three to an order-sync integration leaves two unallocated, and the largest slice any further integration can be granted is one. Reservations are worth setting when one integration is genuinely business-critical and noisy neighbours exist. Oracle’s own guidance is to use the feature only where there is a good reason, because a static allocation converts a shared pool into fixed partitions that cannot flex during a spike.
The AI Connector Service draws from the same pool
The NetSuite AI Connector Service consumes account concurrency exactly like any other integration. Unless an administrator has assigned it a specific concurrency limit on its integration record, it competes for the same pool and is constrained by whatever remains after other integrations take their allocations.
Oracle’s worked example is direct: with a total limit of five concurrent requests and two allocated to a REST web services integration, the AI Connector Service can use only the three that remain unallocated. On a Standard-tier account, connecting an AI client is not a free addition — it is a claim on a pool that was already small.
There is an amplification effect worth planning for. Oracle notes that when a prompt calls an MCP tool, the single tool call is typically preceded by additional protocol-level requests, and those count too. One user action can therefore consume several concurrent slots rather than one. If you are wiring an assistant into NetSuite, size the pool against protocol traffic and not against the number of prompts — the request pattern that produces it is covered in the Model Context Protocol guide. Exceeding the limit here surfaces as a Too Many Requests error and the AI client has to be asked to retry, which makes it the one rejection shape a human notices immediately.
What Oracle actually recommends for retries
Oracle’s published retry guidance is narrower than the exponential-backoff formula usually attributed to it. The documented instruction is to send the first retry after a delay and to increase the delay on subsequent attempts. Oracle explicitly declines to prescribe a length: “There is no recommended delay length to avoid synchronization, so choose a delay that works for your application.”
The stated hazard is synchronisation. Oracle warns against creating artificial concurrency peaks by synchronising retry attempts across threads or applications, and recommends processing retries asynchronously in interactive applications so users are not asked to repeat an action. That threat model is the reason jitter matters more than the growth curve: a fleet of workers that all back off by exactly 1s, 2s, 4s stays perfectly in phase and rebuilds the same peak at every step.
This is where the previous version of this article was weakest in practice. Adding Math.random() * 500 to a delay that grows to 60,000 ms spreads a retry wave across half a second of a minute-long window — roughly 0.8% of the interval, which leaves the fleet effectively synchronised. Full jitter, where the delay is drawn uniformly from zero to the current ceiling, is the form that actually decorrelates callers, and it is the approach documented in the AWS builders’ library reference linked in the sources.
A retry predicate that catches all four shapes
A correct client matches on documented error identifiers rather than on transport status alone, because two of the four rejection shapes are SOAP faults and one arrives as HTTP 400. The predicate below matches the code strings Oracle documents, which is robust to the differing JSON envelopes that RESTlet and REST web services error responses use.
// Concurrency rejection identifiers, per Oracle's "Errors Related to
// Concurrency Violations". Two are SOAP faults, so status alone is not enough.
const CONCURRENCY_CODES = [
'SSS_REQUEST_LIMIT_EXCEEDED', // RESTlet, delivered with HTTP 400
'WS_CONCUR_SESSION_DISALLWD', // SOAP, request-level credentials
'WS_REQUEST_BLOCKED', // SOAP, token-based authentication
];
function isConcurrencyRejection(status, rawBody) {
if (status === 429) return true; // REST web services / AI Connector Service
return CONCURRENCY_CODES.some((code) => rawBody.includes(code));
}
The predicate the retry loop needs: identifier matching, not status matching.
The loop itself applies full jitter. Note that it retries only concurrency rejections — a governance exhaustion or a timeout is an unknown-outcome failure and must not be replayed by this path.
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function callNetSuite(url, init, {
maxAttempts = 5,
baseDelayMs = 1000,
capMs = 32000,
} = {}) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(url, init);
const rawBody = await response.text();
if (!isConcurrencyRejection(response.status, rawBody)) {
return { status: response.status, body: rawBody };
}
if (attempt === maxAttempts - 1) {
throw new Error(
`NetSuite concurrency rejection after ${maxAttempts} attempts: ` +
rawBody.slice(0, 200)
);
}
// Ceiling grows per attempt; the delay is drawn uniformly below it so
// concurrent workers do not re-synchronise into the next peak.
const ceiling = Math.min(baseDelayMs * 2 ** attempt, capMs);
await sleep(Math.random() * ceiling);
}
}
Full jitter: the delay is uniform across zero to the ceiling, not the ceiling plus a small random offset.
Two design points carry the weight here. The function reads the body before deciding, because the decisive evidence is in the payload for three of four shapes. And it throws rather than returning a sentinel on exhaustion, so a caller cannot mistake a dropped write for a successful one — the failure mode that turns a throttling incident into a silent data gap.
SuiteQL paging is a page-size ceiling, not a second concurrency lane
SuiteQL executed over REST web services has no separate concurrency allowance. It draws on the same account pool as every other inbound request. The limits specific to SuiteQL are about result volume, and they are documented: results page at a default and maximum of 1,000 rows per page, across a maximum of 1,000 pages.
One paging rule causes more failed integrations than the ceilings do: offset must be divisible by limit. Oracle’s examples are Offset=20, Limit=10 and Offset=0, Limit=5. Cursor code that advances an offset by a row count returned rather than by the page size will eventually produce an indivisible pair and fail, typically once a page comes back partially filled.
The throughput lesson is that a query returning 5,000 rows costs one concurrent slot for as long as it runs, not five. Consolidating many small reads into one paged query reduces concurrency pressure, which is the opposite of what per-record REST calls do. The query-surface trade-offs are covered in the SuiteQL guide; for concurrency planning the relevant property is simply that one long query occupies one slot.
When to move from per-record calls to batch work
Move to batch when your peak simultaneous request count approaches the account pool, not when you cross a threshold of requests per hour. Hourly volume is the wrong unit — concurrency governs how many requests overlap, so 10,000 requests spread evenly across an hour may never contend, while 200 requests fired in one burst will exhaust a Standard-tier pool immediately.
The measurement that matters is simple: at your busiest moment, how many requests are in flight at once, and what does Integration Governance report as the ceiling. A batch endpoint changes the shape of that number. A RESTlet accepting an array of records processes them in one execution, so 100 records occupy one concurrent slot instead of 100 — at the cost of spending governance units from that RESTlet’s 5,000-unit budget, which is the second gate again.
That trade is the core decision. Batching converts a concurrency problem into a governance problem, and governance problems are solved by sizing the batch so the execution completes within budget, with a remaining-usage check before each record. Work that cannot be bounded that way belongs in a map/reduce script rather than a RESTlet. The interface choice is laid out in the REST web services versus RESTlet comparison, and it is the same decision our NetSuite Integration Pro sync engine makes by batching writes rather than opening a connection per order.
Capacity checklist to run before the spike
Run this before a promotion, a catalog import, or any event that multiplies request volume. Each item is verifiable in an afternoon, and each maps to a failure that shows up first at volume.
- Read the actual limit at Setup > Integration > Integration Governance. Record it. Do not assume 15.
- Count peak simultaneous in-flight requests across all integrations, not just the one being changed — scheduled jobs, the AI Connector Service, and third-party tools share the pool.
- Confirm the retry predicate matches
SSS_REQUEST_LIMIT_EXCEEDED,WS_CONCUR_SESSION_DISALLWDandWS_REQUEST_BLOCKED, not only HTTP 429. - Confirm retries use full jitter, and that no two workers share a fixed delay schedule.
- Check whether any integration record carries a Concurrency Limit allocation; subtract it from the pool available to everything else.
- Verify the sandbox limit separately — it is governed by its own tier and licenses, so a clean sandbox run does not prove production capacity.
- Instrument concurrency rejections as a distinct metric from validation errors, so a 400 carrying a limit code is not filed as a malformed request.
- Confirm every batch RESTlet checks remaining usage before each record, so gate two does not consume what gate one let through.
- Decide the queue depth and the drop policy before the spike, since NetSuite rejects rather than queues — nothing on the server side will absorb the burst for you.
Get the working checklists
The runbooks and decision checklists from these guides, as printable PDFs — free in the SoftXone guide library.
References
- Web Services and RESTlet Concurrency GovernanceOracle NetSuite Help — the account-level statement that the governance limit covers web services and RESTlet requests combined.
- Concurrency Governance Limits Based on Service Tiers and SuiteCloud Plus LicensesOracle NetSuite Help — base limits for both tier vocabularies and the +10 per licence rule, with Oracle’s worked examples.
- Errors Related to Concurrency ViolationsOracle NetSuite Help — the source for HTTP 400 with SSS_REQUEST_LIMIT_EXCEEDED and the two SOAP fault types.
- Effects of Authentication Method on Concurrency GovernanceOracle NetSuite Help — per-user versus per-account governance by authentication method.
- Concurrency Limit per IntegrationOracle NetSuite Help — allocating a slice of the pool, and the minimum-unallocated-value constraint.
- Retrying Failed Web Services RequestsOracle NetSuite Help — Oracle’s own retry guidance and its warning about synchronised retry attempts.
- NetSuite AI Connector Service and Concurrency GovernanceOracle NetSuite Help — the AI Connector Service’s claim on the account pool and the MCP protocol-request note.
- RESTlet Governance and SecurityOracle NetSuite Help — the 5,000-unit per-script RESTlet budget alongside account-level concurrency.
- Collection PagingOracle NetSuite Help — the 1,000-rows-per-page and 1,000-page ceilings and the offset divisibility rule.
- Concurrency Governance — Frequently Asked QuestionsOracle NetSuite Help — shared pool confirmation and the Integration Governance monitoring path.
- Timeouts, Retries and Backoff with JitterAmazon Web Services Builders’ Library — the reference treatment of full jitter and why partial jitter leaves callers correlated.
Frequently asked questions
Will buying a SuiteCloud Plus license fix my throttling?
It raises the ceiling by 10 concurrent requests per license, which helps only if the ceiling is what you are hitting. A SuiteCloud Plus license does nothing for governance unit exhaustion, because units are metered per execution and are unaffected by account concurrency. It also does nothing for a burst-shaped workload that would saturate any pool: if 200 requests are fired simultaneously, moving the limit from 5 to 15 changes which request fails, not whether requests fail. Measure peak simultaneous in-flight requests first, then decide whether the fix is capacity, a client-side queue, or batching.
Does a NetSuite release upgrade change my concurrency limit?
The limit is a function of your service tier and your SuiteCloud Plus license count, not of the release version, so a 2026.1 or 2026.2 upgrade does not move it by itself. What does move it is a tier change, a license purchase, or an administrator allocating part of the pool to a specific integration record. Re-read Setup, Integration, Integration Governance after any of those events rather than assuming the previous number carried over. Production, sandbox and release preview accounts are each governed by their own limit, so a sandbox figure never proves a production one.
How much of the account pool should one integration use?
Less than all of it, and the reason is that the pool is shared with traffic you may not be counting. Scheduled jobs calling in, third-party tools, an AI client, and every other integration draw from the same account limit. A practical starting point is to size a single integration for a clear majority of the pool only when it is the sole consumer, and to leave explicit headroom otherwise. Where one integration is genuinely business-critical, an explicit Concurrency Limit allocation on its integration record is more predictable than hoping the shared pool has room during a spike.
Do user event scripts triggered by my integration consume concurrency?
No. Account concurrency counts inbound web services and RESTlet requests, and a user event script firing as a side effect of a record your integration created is not a separate inbound request. It does consume governance units, within its own 1,000-unit budget, and that cost is easy to miss because it is invisible from the integration side. This is a common month-two surprise: another team deploys a user event script on sales orders, and integration writes that previously succeeded begin failing on unit exhaustion with no change to the integration itself.
Does NetSuite ever return HTTP 429?
Yes, but not for the case most retry code assumes. Oracle documents a RESTlet concurrency rejection as HTTP 400 carrying SSS_REQUEST_LIMIT_EXCEEDED, and SOAP concurrency rejections as fault types rather than status codes. A Too Many Requests error is what Oracle describes for the AI Connector Service when a request exceeds the concurrency limit. The safe design is to treat 429 as one of several signals rather than the signal: match on the documented error identifiers as well, so a rejection is retried whichever surface produced it.

Leave a Reply