Global Quota
The global quota is the shared SES sending cap for the trailing 24 hours, aggregated across all tenants and all send sources under the account. Use it to route high-volume campaigns through Outbound up to the shared cap, then fall back to another vendor (e.g. SendGrid) once you're near it.
This is distinct from dashboard.quota(), which is per-tenant and for reporting only.
Sends are NOT gated.
email.sendandemail.bulkalways submit — there is no global-quota gate on the send path. Respecting the shared cap is opt-in: callquota.check(count)before you send and route on the result.The window is rolling 24h (matching AWS SES): there is no reset event — capacity returns continuously as individual sends age past 24h. Route on
availableonly; never wait on a reset time.
All examples assume you've set the API key in the constructor. Any valid tenant key may read or check — the pool is account-wide. See Configuration.
Check the Quota (opt-in)
quota.check(count) is the primitive you want. It atomically decides whether count fits in the shared pool and holds it, so concurrent callers can't both grab the same headroom. A plain read can't do this — two campaigns reading the same value both decide "it fits" and overshoot.
const recipientCount = 300;
const { ok, available, shortfall } = await outbound.quota.check(recipientCount);
if (ok) {
await outbound.email.bulk({ fromEmail, emailSubject, emails: batch });
} else {
await sendViaSendGrid(batch); // not enough shared headroom; `shortfall` = how much over
}
// Nothing to release — the hold auto-expires.Why a hold (the concurrency story)
Remaining = 500
Req A: check(300) → 500 ≥ 300 → HOLD 300 → { ok: true, available: 200 }
Req B: check(300) → 200 < 300 → no hold → { ok: false, available: 200, shortfall: 100 }The check runs as a single atomic server operation, so A's hold is visible to B with no race. The hold self-expires after ttlSeconds (the SES propagation window) — by then the send has shown up in SES's own counter, so there is no reservationId and nothing to release.
Response
{
ok: true, // true = `count` was held; safe to send up to `count`
available: 200, // headroom remaining AFTER this call's hold
ttlSeconds: 180, // how long the hold lasts if you do nothing
shortfall: 100, // present only when ok:false — count - available
degraded: true // present only on a fail-closed response (see below)
}| Field | Type | Description |
|---|---|---|
ok | boolean | true = count held against the pool; send up to count. false = route to fallback. |
available | number | Shared headroom remaining after this call's hold. |
ttlSeconds | number | Hold lifetime if you do nothing. |
shortfall | number? | Present when ok:false: count - available. |
degraded | boolean? | Present (true) when the quota service was degraded and failed closed. |
Fail-closed. If the quota service can't compute headroom (Redis/SES error),
checkreturns{ ok: false, degraded: true }(HTTP 200, not a 5xx) so a degraded service makes you fall back rather than overshoot. A400is only for a badcount.Call
checkonce per intended send — eachok:trueplaces a fresh hold.
Read the Global Quota
A cheap, side-effect-free snapshot — good for dashboards or a rough enqueue-time hint. For anything concurrency-sensitive use check(), which holds.
const q = await outbound.quota.global();
console.log(`Available: ${q.available} of ${q.limit} reservable (${q.percentageUsed}% of ${q.hardLimit} used)`);Response Fields
| Field | Type | Description |
|---|---|---|
hardLimit | number | Real SES global cap for the window. Server-driven. |
systemReservePercent | number | % of hardLimit held back for ungated system sends. |
limit | number | Effective reservable cap = floor(hardLimit * (1 - systemReservePercent/100)). |
used | number | Accepted/sent plus in-flight (queued + processing) recipients, all sources. |
reserved | number | Sum of live (un-consumed, un-expired) holds. |
inFlight | number? | Queued + processing recipients folded into used. |
available | number | max(0, limit - used - reserved). Route on this. |
remaining | number? | max(0, hardLimit - used). Informational only — do not route on it. |
percentageUsed | number? | used / hardLimit * 100. |
window | string | rolling_24h (SES) or calendar_day_utc. |
nextCapacityFreesAt | string | null | Informational only; null under rolling-24h. |
perSecondLimit | number? | Current global send-rate ceiling (emails/sec). |
asOf | string | ISO 8601 snapshot time. |
Migrating from removed send-time gating
Earlier versions exposed quota.reserve() / quota.release() and a reserveFromGlobalQuota flag / reservationId param on email.send / email.bulk. These have all been removed — sends were never actually gated by them. Use an explicit check() before sending instead:
// OLD (no longer gates):
// await outbound.email.bulk({ ...batch, reserveFromGlobalQuota: true });
// NEW:
if ((await outbound.quota.check(batch.length)).ok) {
await outbound.email.bulk({ ...payload, emails: batch });
} else {
await sendViaSendGrid(batch);
}Possible Errors
| Error | Status | Cause |
|---|---|---|
BadRequestError | 400 | count missing / not a positive integer |
AuthenticationError | 401 | Invalid or missing API key |
RateLimitError | 429 | Read/check rate limit exceeded |