Skip to main content

Rate limiting

Traceable enforces rate limits on all API endpoints. Rate limits protect platform stability and ensure fair access across all integrations.

Current limits

Rate limiting is applied in three independent layers. A request can be limited by any layer that applies to it; the effective ceiling on a given route is the tightest layer.

LayerApplies toKeyLimit
Edge (all API traffic)Every /api/* route (/api/health exempt)Per IP address120 requests / minute (fixed window)
Public-DPP routeGET /api/dpp/{slug}Per IP address100 requests / minute
Integration API/api/v1/*Per API key100 requests / minute (reads and writes)
  • Public DPP reads (GET /api/dpp/{slug}) pass through both the edge layer (120/min) and the route layer (100/min). The effective ceiling is the tighter of the two: min(120, 100) = 100 requests / minute per IP.
  • The Integration API (/api/v1) is limited per API key at 100 requests / minute, counting both reads and writes. Live (pk_live_) and sandbox (pk_test_) keys have separate quotas, so exercising a sandbox key never consumes your live budget. See Integration API: Errors and conventions.
  • GET /api/health is exempt from rate limiting so liveness probes are never throttled.

Limits are applied on a fixed-window basis — the counter resets at the end of each window.

Rate limit response headers

Every API response includes the following headers to allow your integration to track its usage. Traceable uses the IETF RateLimit-* header names:

HeaderTypeDescription
RateLimit-LimitintegerThe maximum number of requests allowed in the current window
RateLimit-RemainingintegerThe number of requests remaining in the current window
RateLimit-ResetintegerSeconds remaining until the current window resets (delta-seconds, not a Unix timestamp)

Example headers on a healthy response:

HTTP/2 200 OK
Content-Type: application/json
RateLimit-Limit: 100
RateLimit-Remaining: 47
RateLimit-Reset: 23

Monitor RateLimit-Remaining in your integration. When it approaches zero, slow your request rate proactively before hitting the limit.

429 Too Many Requests

When a rate limit is exceeded, the API returns:

HTTP Status: 429 Too Many Requests

Response headers:

HTTP/2 429 Too Many Requests
Content-Type: application/json
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 23
Retry-After: 23

Response body:

{
"error": "Rate limit exceeded",
"code": "RATE_LIMITED"
}

Read the wait time from the Retry-After response header (number of seconds until the window resets and requests are accepted again). There is no retryAfter field in the response body — do not read one.

For integrations that may hit rate limits, implement exponential backoff with jitter:

async function fetchWithBackoff(
url: string,
options?: RequestInit,
maxRetries = 4
): Promise<Response> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);

if (response.status !== 429) {
return response; // success or non-rate-limit error
}

if (attempt === maxRetries) {
return response; // exhausted retries, return the 429
}

// Read retry-after from the response header (there is no body field)
const retryAfter = Number(response.headers.get('Retry-After')) || 60;

// Exponential backoff: 1s, 2s, 4s, 8s...
const baseDelay = Math.pow(2, attempt) * 1000;
// Add jitter: random additional 0–1000ms to avoid thundering herd
const jitter = Math.random() * 1000;
// Respect the server's retryAfter if it's longer than our computed delay
const delay = Math.max(baseDelay + jitter, retryAfter * 1000);

console.warn(
`Rate limited (attempt ${attempt + 1}/${maxRetries}). ` +
`Retrying in ${Math.round(delay / 1000)}s...`
);

await new Promise(resolve => setTimeout(resolve, delay));
}

throw new Error('Should not reach here');
}

// Usage
const response = await fetchWithBackoff(
'https://app.traceable.digital/api/dpp/swiftvolt-48v-100ah-ev-pack'
);
import time
import random
import requests


def fetch_with_backoff(url: str, max_retries: int = 4, **kwargs) -> requests.Response:
"""
Fetch a URL with exponential backoff on 429 responses.

Args:
url: The URL to request.
max_retries: Maximum number of retries on rate limit. Default 4.
**kwargs: Additional arguments passed to requests.get().

Returns:
The final requests.Response object.
"""
for attempt in range(max_retries + 1):
response = requests.get(url, timeout=10, **kwargs)

if response.status_code != 429:
return response

if attempt == max_retries:
return response # exhausted retries

# Read retry-after from the response header (there is no body field)
try:
retry_after = int(response.headers.get("Retry-After", 60))
except (TypeError, ValueError):
retry_after = 60

# Exponential backoff with jitter
base_delay = (2 ** attempt) # seconds: 1, 2, 4, 8
jitter = random.uniform(0, 1)
delay = max(base_delay + jitter, retry_after)

print(
f"Rate limited (attempt {attempt + 1}/{max_retries}). "
f"Retrying in {delay:.1f}s..."
)
time.sleep(delay)

# unreachable
raise RuntimeError("fetch_with_backoff: should not reach here")

Bulk operations

If you need to fetch a large number of DPPs (for example, populating a product registry or running a compliance audit), follow these guidelines:

  1. Space requests — stay comfortably below the 100 requests/minute ceiling to give yourself headroom for retries
  2. Use caching — DPPs change infrequently. Cache responses with a TTL of at least 5 minutes and check updatedAt to invalidate when needed. This dramatically reduces the requests needed for repeat access patterns.
  3. Parallelise conservatively — use a concurrency limit of 3–5 simultaneous requests rather than firing all requests at once
  4. Contact support for higher limits — if your use case legitimately requires more than 100 requests/minute (for example, a national product registry that needs to bulk-sync all battery DPPs), contact support@traceable.digital with details of your use case and volume requirements. Higher limits are available for approved integrations.

Example of rate-limited bulk fetching in JavaScript:

import PQueue from 'p-queue'; // npm install p-queue

const queue = new PQueue({
concurrency: 3, // max 3 concurrent in-flight requests
interval: 1000, // per interval (ms)
intervalCap: 1, // 1 request/second = 60/minute — well under the 100/minute public-DPP ceiling
});

async function bulkFetchDpps(slugs: string[]): Promise<Map<string, unknown>> {
const results = new Map<string, unknown>();

await Promise.all(
slugs.map(slug =>
queue.add(async () => {
try {
const response = await fetchWithBackoff(
`https://app.traceable.digital/api/dpp/${slug}`
);
if (response.ok) {
results.set(slug, await response.json());
} else {
console.warn(`Failed to fetch DPP for ${slug}: ${response.status}`);
}
} catch (err) {
console.error(`Error fetching DPP for ${slug}:`, err);
}
})
)
);

return results;
}

Rate limit key behaviour

  • The edge and public-DPP layers are keyed per IP address. Changing your IP address does not reset an Integration API key's per-key counter.
  • The Integration API layer is keyed per API key, and counts reads and writes alike. Multiple integrations using different API keys have independent counters, and a key's live (pk_live_) and sandbox (pk_test_) quotas are tracked separately.