Rate Limits & Quotas
To keep the service available for all organizations, the Ohio Sales Tax API enforces two tiers of rate limiting:
- Per-Second Burst Limit: Sliding 1-second window preventing concurrent request spikes.
- Monthly Request Quota: Monthly volume allocated to your organization tier.
Plan Limits
| Plan Tier | Sliding Window (req/sec) | Monthly Request Quota |
|---|---|---|
| Developer / Beta | 10 req/s | 1,000 / month |
| Ohio Sales Tax Pro | 50 req/s | 50,000 / month |
| Enterprise / POS | 250+ req/s | Unlimited |
Response Headers
Every HTTP response from /api/v1/rates includes standard RFC rate limit headers:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Maximum requests permitted within the 1-second window. | 10 |
X-RateLimit-Remaining | Remaining requests available in the current window. | 7 |
X-RateLimit-Reset | Unix timestamp (in seconds) when the current window refreshes. | 1791036000 |
Retry-After | Returned on 429 status codes. Number of seconds to pause before retrying. | 1 |
Rate Limit Exceeded (429 Too Many Requests)
When an application exceeds the sliding window or monthly quota, the API responds with:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 1
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791036001
{
"type": "https://ohiosalestaxcalculator.com/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit of 10 requests per second exceeded. Please back off and retry.",
"request_id": "req_rl_49b1a0"
}
Production Retry Pattern (Exponential Backoff with Jitter)
Always implement exponential backoff with full jitter when consuming the API in high-concurrency production environments:
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3): Promise<Response> {
let attempt = 0;
while (attempt < maxRetries) {
const res = await fetch(url, options);
if (res.status === 429) {
attempt++;
const retryAfterHeader = res.headers.get("Retry-After");
const retryAfterSeconds = retryAfterHeader ? parseInt(retryAfterHeader, 10) : 1;
// Add full jitter: random delay between 0 and exponential backoff
const baseDelay = retryAfterSeconds * 1000 * Math.pow(2, attempt);
const jitterDelay = Math.random() * baseDelay;
console.warn(`[429] Rate limit reached. Retrying attempt ${attempt}/${maxRetries} after ${jitterDelay.toFixed(0)}ms`);
await new Promise((resolve) => setTimeout(resolve, jitterDelay));
continue;
}
return res;
}
throw new Error(`Max retries (${maxRetries}) exceeded for ${url}`);
}
import time
import random
import requests
def fetch_with_retry(url, headers, params, max_retries=3):
attempt = 0
while attempt < max_retries:
res = requests.get(url, headers=headers, params=params)
if res.status_code == 429:
attempt += 1
retry_after = int(res.headers.get("Retry-After", 1))
jitter_delay = random.uniform(0.5, 1.5) * (retry_after * (2 ** attempt))
print(f"[429] Retrying after {jitter_delay:.2f}s...")
time.sleep(jitter_delay)
continue
return res
raise Exception(f"Max retries exceeded for {url}")