Metering, Quotas & Rate Limits
This page covers how many points a request costs, what the free tier includes, and the 429 response you get when a limit is hit. API calls are currently metered on the free tier: usage is recorded but not charged.
1. Billable points
Only successful /v1/forecast requests are metered:
billable points = usage.input_data_points + usage.forecast_data_points| Field | What it counts |
|---|---|
usage.input_data_points | Observations actually fed to the model. Points dropped by a long-gap split are not counted, and neither are interpolated values or inserted timestamps. |
usage.forecast_data_points | Forecast points, that is h × entities. |
For example, one entity with 48 hourly observations and h = 24 costs 48 + 24 = 72 points. If 2 of those timestamps were missing and got filled, it is still counted as 46 observations: 46 + 24 = 70 points.
- Error responses are never metered, including 4xx, 429 and 5xx.
/v1/cleanis not metered and does not use the daily quota.- Client timeouts: if the request succeeded on the server, it is still metered; see the retry note in Authentication & Headers.
2. Free tier
| Item | Free tier |
|---|---|
/v1/forecast requests per minute (RPM) | 10 |
/v1/forecast points per day | 1,000,000 (resets at 00:00 UTC, 08:00 Beijing time) |
/v1/forecast per request | At most 200,000 rows |
/v1/clean | Free, 60 calls per minute per account, at most 200,000 rows per call |
The request ceiling that actually applies to your account is the X-RateLimit-Limit header on any response to any of its keys (see section 4).
3. Limits enforced today
Every limit is per account: all keys of one account share a single per-minute request window and a single daily point quota, so creating more keys does not add capacity. Different accounts do not share limits.
X-RateLimit-Policy | Endpoint | Counts | Window and reset |
|---|---|---|---|
forecast-rpm | /v1/forecast | Requests, at most 10 | 60-second sliding window |
clean-rpm | /v1/clean | Requests, at most 60 | 60-second sliding window |
forecast-daily-points | /v1/forecast | Points, at most 1,000,000 | Per UTC calendar day, resets at UTC midnight (08:00 Beijing time) |
- Request limits are checked after authentication and before the body is parsed, so calls with a bad body still count. The rate-limited request itself is not recorded.
- Daily points are reserved when the request starts, as "rows +
h× entities"; on success the reservation is settled to the actual billable points, and on failure it is released in full. Whether a request passes therefore depends on whether its estimate fits in today's remaining quota. - Per-request caps are not rate limits:
/v1/forecasttakes at most 200,000 rows (seriesplusfuture_covariates) and/v1/cleanat most 200,000; above that you get 422POINTS_EXCEEDEDright away, before any cleaning or quota use. The cap follows the memory of the cleaning host, so one oversized request is refused rather than queued; split it into several requests. - Per-series context is not a rate limit either: an entity longer than the selected model's maximum context returns 422
SERIES_TOO_LONGand is not billed. History is never cut for you; see Models & Limits. - Cleaning capacity is shared by all accounts and is not a rate limit either: only so much data is cleaned at once. While it is full,
/v1/forecastqueues briefly;/v1/cleandoes not queue and returns 503SERVICE_UNAVAILABLEwithRetry-After(the call still counts againstclean-rpm). Retry after the indicated interval.
4. The 429 response
When a request limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1790665223
X-RateLimit-Limit-Type: rpm
X-RateLimit-Policy: clean-rpm
X-Request-Id: req_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "rate_limited",
"retryable": true,
"message": "Too many requests for this account; please respect the Retry-After interval",
"request_id": "req_7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d",
"details": {"limit_type": "rpm", "limit_scope": "account", "limit_value": 60, "retry_after_seconds": 23}
}
}| Header | Request limit (rpm) | Daily points (daily) |
|---|---|---|
Retry-After | Seconds until the window has room | Seconds until the next UTC midnight |
X-RateLimit-Limit | Requests per minute | Points per day |
X-RateLimit-Remaining | 0 | Points left today |
X-RateLimit-Reset | Unix time (seconds) when the window has room | Unix time (seconds) of the next UTC midnight |
X-RateLimit-Limit-Type | rpm | daily |
X-RateLimit-Policy | forecast-rpm or clean-rpm | forecast-daily-points |
The X-RateLimit-* headers are not limited to 429. Once the key is authenticated, every /v1/forecast and /v1/clean response carries them, successes and other errors included, describing the request limit (rpm) of the key's account after the current request: X-RateLimit-Remaining is how many more requests fit in the current 60-second window, so you can slow down before being refused. Requests made with the account's other keys count down the same window. Only a 429 reports the bucket that was exhausted, which may be daily. Responses to a rejected key (401) carry none of them.
On a 429, details.limit_scope is always account: the exhausted limit belongs to the account, so retrying with another key of the same account does not get through.
When the daily quota does not fit, details also carries the reason and the request's estimate:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"category": "rate_limited",
"retryable": true,
"message": "Daily point quota of this account exhausted",
"request_id": "req_2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e",
"details": {
"limit_type": "daily",
"limit_scope": "account",
"limit_value": 1000000,
"retry_after_seconds": 30512,
"reason": "daily_quota",
"requested_points": 2400
}
}
}If requested_points is larger than X-RateLimit-Remaining, a smaller request (fewer entities or shorter history) can still fit in today's remaining quota.
5. Client retries
Request limits suit an automatic retry after Retry-After; the daily quota only frees up at UTC midnight, so do not wait for it in-process.
import time
import requests
def retryable(resp):
"""error.retryable: whether the same request may succeed when sent again."""
try:
return resp.json()["error"]["retryable"] is True
except (ValueError, KeyError, TypeError):
return False
def post_with_retry(url, payload, headers, attempts=4):
for attempt in range(attempts):
resp = requests.post(url, json=payload, headers=headers, timeout=180)
if resp.ok or not retryable(resp) or attempt == attempts - 1:
return resp
# The daily point quota only frees up at the next UTC midnight: surface it, do not sleep.
if resp.headers.get("X-RateLimit-Limit-Type") == "daily":
return resp
# 429 and MODEL_BUSY carry Retry-After; the other retryable errors usually do not.
time.sleep(int(resp.headers.get("Retry-After", 2 ** attempt)))
return resp