Rate limits¶
Tango enforces rate limits to keep the API fast and reliable for everyone. Limits apply at the account level: requests made via API keys and OAuth2 tokens for the same account all draw from the same quotas.
Where to see your limits and usage¶
- Your current plan limits + near-real-time usage: Account profile
- Upgrade pricing / higher limits: Pricing
How rate limits work (burst + daily)¶
Most plans have more than one rate limit window:
- Burst: short window (e.g. per minute) that protects the API from sudden spikes
- Daily: fixed window that resets at midnight UTC, caps total volume per day
You may be “fine” on daily usage but still hit burst limits (or vice versa).
Rate limit headers¶
Every /api/* response includes rate limit headers.
Overall headers (most restrictive window)¶
These headers summarize the most restrictive window (the one you’re closest to hitting):
X-RateLimit-Limit: total requests allowed for that windowX-RateLimit-Remaining: requests remaining in that windowX-RateLimit-Reset: seconds until reset for that window
Per-window headers (daily, burst, etc.)¶
For each configured window (commonly Daily and Burst), you’ll also see:
X-RateLimit-Daily-Limit,X-RateLimit-Daily-Remaining,X-RateLimit-Daily-ResetX-RateLimit-Burst-Limit,X-RateLimit-Burst-Remaining,X-RateLimit-Burst-Reset
Each *-Reset value is seconds until that specific window resets.
Quick header check (curl)¶
curl -s -D - -o /dev/null \
-H "X-API-KEY: your-api-key-here" \
"https://tango.dev/api/contracts/?limit=1"
Example response headers:
X-RateLimit-Daily-Limit: 1500
X-RateLimit-Daily-Remaining: 1450
X-RateLimit-Daily-Reset: 18000
X-RateLimit-Burst-Limit: 100
X-RateLimit-Burst-Remaining: 95
X-RateLimit-Burst-Reset: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 45
X-Execution-Time: 0.045s
Here X-RateLimit-Daily-Reset: 18000 means there are 5 hours left in the current UTC day. It does not mean you have been throttled — it is simply how much of today is left.
Understanding the daily reset¶
The daily window is a fixed calendar-day bucket, not a rolling 24 hours. It refills at midnight UTC no matter when you started sending requests.
This has one consequence worth planning around: X-RateLimit-Daily-Reset tells you how much of the current UTC day is left, so the wait you get after exhausting your daily quota depends on when you exhaust it.
- Exhaust your daily quota at 19:00 UTC and you wait ~5 hours for the reset.
- Exhaust the very same quota at 02:00 UTC and you wait ~22 hours.
Midnight UTC is 8:00 PM Eastern (Mar–Nov) or 7:00 PM Eastern (Nov–Mar). If you run in a US timezone, your quota therefore refills in the evening and is spent over the course of the business day — so a heavy morning can leave you throttled for the rest of the afternoon. If you have a large daily batch job, running it just after the midnight-UTC reset gives it the full day's quota to draw from.
Premium query limits (select endpoints)¶
Some endpoint families carry their own premium query budget instead of your account-wide limits. A request to one of those endpoints draws from that budget and does not count against your daily or per-minute allowance.
How premium limits work¶
- Separate budget: The premium budget exists only for designated endpoint families. Standard endpoints use your account-wide quotas.
- Replaces your standard limits: on these endpoints the premium budget is the only limit that applies — your daily and per-minute quotas are neither checked nor spent.
- Shared across endpoint family: Multiple endpoints may share a single premium counter. For example, the CALC labor-rate endpoints (
GET /api/idvs/{key}/lcats/andGET /api/entities/{uei}/lcats/) share one premium budget. - Counts requests, not rows: Each API call costs one request, regardless of pagination or the number of rows returned.
- Fixed daily window: The premium budget resets daily at 00:00 UTC.
Premium headers¶
On responses from endpoints with a premium budget (including 429s), you'll see:
X-RateLimit-Premium-Limit: total requests allowed in the premium budgetX-RateLimit-Premium-Remaining: requests remaining in the premium budgetX-RateLimit-Premium-Reset: seconds until reset for the premium budget
These headers appear only on requests to endpoints with a premium budget — they are absent everywhere else.
Finding your premium limits¶
Check your account profile to see your current premium usage and limits per endpoint family. To request a higher limit, email [email protected].
What happens when you exceed a limit (HTTP 429)¶
When you hit a rate limit, Tango responds with HTTP 429 and a JSON body like:
{
"detail": "Rate limit exceeded for burst. Please try again in 45 seconds.",
"wait_in_seconds": 45
}
Recommended client behavior¶
- Stop retrying immediately after a 429.
- Sleep for at least
wait_in_seconds(preferred) orX-RateLimit-Reset. - But cap how long you are willing to sleep. A burst 429 clears in under a minute; a daily 429 can report a wait of many hours (however much of the UTC day is left). Blindly sleeping on that value will park your process until the small hours. Treat a multi-hour wait as "daily quota exhausted" — log it, alert, and stop, rather than sleeping through it.
- Then retry with exponential backoff + jitter to avoid a thundering herd.
Python example¶
import random
import time
import httpx
url = "https://tango.dev/api/contracts/?limit=1"
headers = {"X-API-KEY": "your-api-key-here"}
# Never block the process for hours. A burst 429 clears in under a minute, but
# a *daily* 429 can report a wait of many hours (until midnight UTC) — that is
# a quota problem to surface, not to sleep through.
MAX_SLEEP_SECONDS = 120
backoff = 1.0
for _ in range(10):
r = httpx.get(url, headers=headers)
if r.status_code != 429:
r.raise_for_status()
break
body = r.json()
wait = body.get("wait_in_seconds")
if wait is None:
wait = r.headers.get("X-RateLimit-Reset")
if wait is not None:
wait = float(wait)
if wait > MAX_SLEEP_SECONDS:
raise RuntimeError(
f"Daily quota exhausted; resets in {wait / 3600:.1f}h "
f"(midnight UTC). Upgrade your plan or retry after the reset."
)
time.sleep(wait)
continue
time.sleep(backoff + random.random())
backoff = min(backoff * 2, 60)
JavaScript example¶
This example assumes fetch is available (modern browsers or Node 18+).
(async () => {
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const url = "https://tango.dev/api/contracts/?limit=1";
const headers = { "X-API-KEY": "your-api-key-here" };
// Never block for hours. A burst 429 clears in under a minute, but a *daily*
// 429 can report a wait of many hours (until midnight UTC) — that is a quota
// problem to surface, not to sleep through.
const MAX_SLEEP_SECONDS = 120;
let backoffMs = 1000;
for (let i = 0; i < 10; i++) {
const r = await fetch(url, { headers });
if (r.status !== 429) {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
break;
}
const body = await r.json();
const wait = body.wait_in_seconds ?? r.headers.get("X-RateLimit-Reset");
if (wait != null) {
const waitSeconds = Number(wait);
if (waitSeconds > MAX_SLEEP_SECONDS) {
throw new Error(
`Daily quota exhausted; resets in ${(waitSeconds / 3600).toFixed(1)}h ` +
`(midnight UTC). Upgrade your plan or retry after the reset.`,
);
}
await sleep(waitSeconds * 1000);
continue;
}
await sleep(backoffMs + Math.random() * 250);
backoffMs = Math.min(backoffMs * 2, 60_000);
}
})().catch((err) => {
console.error("Request failed:", err);
});
Reduce calls (and avoid limits) in practice¶
- Use response shaping (
shape=) to avoid extra “follow-up” requests. See the Response Shaping Guide. - Paginate responsibly; avoid re-fetching the same pages repeatedly.
- Cache hot lookups on your side when appropriate (e.g. “entity by UEI”).
- Prefer webhooks for event-driven updates instead of polling where possible.