Rate limits
Per-tier request budgets, the response headers, and how the SDK handles 429s.
Every API key is rate-limited by its organization’s plan, over a 1-hour sliding window, and each key has its own window. The SDK auto-retries short waits; longer waits raise so your code can decide.
Source: _http/retry.py
Per-tier limits
| Tier | Requests / hour |
|---|---|
| Free | 2,000 |
| Core | 5,000 |
| Pro | 20,000 |
| Team | 50,000 |
| Enterprise | 100,000 |
A key’s limit follows the plan: an upgrade applies to existing keys immediately. An admin can hold a single key below the plan with an optional per-key cap in requests per hour - rate_limit when creating or updating a key. A cap can never raise a key above its plan. Removing it (clear_rate_limit=True in the SDK, "rate_limit": null over REST) returns the key to its plan. Settings and every API-key response report the limit the key actually gets.
Response headers
A 429 response carries the state of the window. Successful responses do not include rate-limit headers - pace against the per-tier limit above, and let the SDK handle a 429 for you.
| Header | Meaning |
|---|---|
Retry-After |
Seconds until the next call may succeed |
X-RateLimit-Limit |
The key’s limit for the window |
X-RateLimit-Remaining |
Calls left in the window (0 on a 429) |
X-RateLimit-Reset |
Unix timestamp when the next call may succeed |
What counts
One HTTP request → one count. Payload size doesn’t matter. Streaming downloads (image / model / export blobs) count as a single request regardless of size.
Bulk operations are designed to keep counts low - prefer client.batch.move() / client.batch.update() over per-image calls in a loop.
SDK auto-retry
RateLimitError carries a retry_after attribute. The SDK waits and retries automatically when the response includes a Retry-After header and the wait is at most 120 seconds. Anything longer raises immediately so your code can back off, queue, or fail.
from pictograph.exceptions import RateLimitError
import time
try:
client.datasets.list(
limit=1000,
)
except RateLimitError as e:
print(f"Hit cap; retry in {e.retry_after}s")
time.sleep(e.retry_after or 5.0)
# …then retry. The SDK won't auto-recover for waits >120s.
The pictograph CLI inherits the same behaviour. On a long wait it prints the retry estimate to stderr so you can Ctrl-C if you don’t want to wait.
Spreading bursty load
If your workload is bursty (nightly imports, large auto-annotation runs), pace it across the hour:
from time import sleep
for batch in batches:
process(batch)
sleep(0.8) # at most ~4,500 req/hr - under Core's 5,000
For sustained workloads above your tier, the right move is to upgrade - retrying harder doesn’t increase your share.
See also
- Error handling - the full exception hierarchy
- Credits - paid-operation budgeting