Rate Limiting & Response Headers
Understanding rate limits and response headers is crucial for building reliable integrations with the OilPriceAPI.
Rate Limits by Plan
Two independent limits apply:
- A request quota, which varies by plan — monthly on every paid plan, daily on Free. Exceeding it returns
402 Payment Required. - A request rate limit of 60 requests per rolling 60-second window, per API key, which is the same on every plan. Exceeding it returns
429 Too Many Requests.
The rate limit is a window, not a per-second cap. Ten requests fired in the same second are fine — you are only throttled once you have made more than 60 requests in the preceding 60 seconds. There is no requirement to space requests one second apart.
| Plan | Request Quota | Request Rate Limit |
|---|---|---|
| Free | 50 / day | 60 per rolling 60s |
| Free Trial (7 days) | 10,000 | 60 per rolling 60s |
| Developer | 10,000 / month | 60 per rolling 60s |
| Starter | 50,000 / month | 60 per rolling 60s |
| Professional | 100,000 / month | 60 per rolling 60s |
| Scale | 1,000,000 / month | 60 per rolling 60s |
Plan prices are on the pricing page. Plans differ by request quota, not by request rate.
Other Rate-Limit Lanes
A few request classes have their own limits:
| Request class | Limit | Keyed by |
|---|---|---|
Authenticated /v1/* (the default) | 60 per rolling 60s | API key |
Unauthenticated /v1/* (e.g. demo endpoints) | 10 per rolling 60s | Client IP |
| Authenticated but over monthly quota | 5 per rolling 60s | API key |
| Marine-fuel endpoints | 30 per rolling 60s | API key |
| Marine-fuel historical | 5 per rolling 60s | API key |
If you have blown through your monthly quota, your rate limit drops from 60/min to 5/min until the quota resets — so a client that ignores 402 responses and keeps hammering will also start seeing 429.
Free Trial
New accounts receive a 7-day free trial with:
- 10,000 API requests during the trial period
- Commodity access according to the account's dataset entitlements
- Full API functionality (Professional-level features)
- No credit card required
After the trial ends, the account continues on the Free tier with 50 requests/day. Upgrade when you need higher monthly quotas or paid-tier features.
Note: The request rate limit is 60 requests per rolling 60-second window on all plans; exceeding it returns 429. Monthly quotas reset on the 1st of each month at 00:00 UTC. Trial quota converts when you upgrade to a paid plan.
Rate Limit Headers
Every API response includes headers that help you monitor your usage:
Rate Limit Headers
Every response includes headers that track your monthly quota:
| Header | Description | Example |
|---|---|---|
X-RateLimit-Limit | Your plan's monthly request quota | 10000 |
X-RateLimit-Remaining | Requests remaining this month | 9876 |
X-RateLimit-Used | Requests used this month | 124 |
X-RateLimit-Reset | Unix timestamp when the monthly quota resets | 1785542399 |
X-RateLimit-Tier | Your current plan tier | developer |
X-Request-Id | Unique request ID — include it in support tickets | 8c1fc2b1-… |
Pagination Headers
When retrieving paginated data (historical prices, etc.):
| Header | Description | Example |
|---|---|---|
X-Total | Total number of records available | 2016 |
X-Total-Pages | Total number of pages | 21 |
X-Page | Current page number | 1 |
X-Per-Page | Number of records per page | 100 |
Link | RFC 5988 pagination links | See below |
Link Header Format
Link: <https://api.oilpriceapi.com/v1/prices/past_week?page=1>; rel="first",
<https://api.oilpriceapi.com/v1/prices/past_week?page=2>; rel="next",
<https://api.oilpriceapi.com/v1/prices/past_week?page=21>; rel="last"
Diagnostic Headers
| Header | Description | Example |
|---|---|---|
X-Request-Id | Unique request identifier for support | df77d32e-f606-4f69-8dee-4508439fcb79 |
X-Cache | Cache status (HIT/MISS) | HIT |
Quote the X-Request-Id from an affected response when contacting support — it lets us trace the exact request.
Handling Rate Limits
Check Headers Before Making Requests
import requests
import time
class RateLimitedClient:
def __init__(self, api_key):
self.api_key = api_key
self.remaining = None
self.reset_time = None
def make_request(self, endpoint, params=None):
# Check if we need to wait
if self.remaining == 0 and self.reset_time:
wait_time = self.reset_time - time.time()
if wait_time > 0:
print(f"Rate limited. Waiting {wait_time:.0f} seconds...")
time.sleep(wait_time)
response = requests.get(
f'https://api.oilpriceapi.com/v1{endpoint}',
headers={'Authorization': f'Token {self.api_key}'},
params=params
)
# Update rate limit info
self.remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
self.reset_time = int(response.headers.get('X-RateLimit-Reset', 0))
return response
Implement Exponential Backoff
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);
// Check rate limit headers
const remaining = response.headers.get("X-RateLimit-Remaining");
const resetAfter = response.headers.get("X-RateLimit-Reset-After");
console.log(`Remaining requests: ${remaining}`);
if (response.status === 429) {
// Rate limited - wait and retry
const waitTime = parseInt(resetAfter || "60") * 1000;
console.log(`Rate limited. Waiting ${waitTime}ms...`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
continue;
}
if (response.ok) {
return response;
}
// Exponential backoff for other errors
const delay = Math.min(1000 * Math.pow(2, i), 10000);
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error("Max retries exceeded");
}
Batch Requests Efficiently
One request can carry up to 20 commodity codes, and it counts as one request against your quota. This is the single most effective thing you can do with your allowance.
def fetch_multiple_commodities_efficiently():
# ❌ Three requests, three against quota
# for code in ['WTI_USD', 'BRENT_CRUDE_USD', 'NATURAL_GAS_USD']:
# requests.get(f'/v1/prices/latest?by_code={code}')
# ✅ One request, ONE against quota — up to 20 codes
response = requests.get(
'https://api.oilpriceapi.com/v1/prices/latest',
headers={'Authorization': 'Token YOUR_API_KEY'},
params={'by_code': 'WTI_USD,BRENT_CRUDE_USD,NATURAL_GAS_USD'}
)
return response.json() # -> data.prices[] when you pass more than one code
A single code returns a flat data object; two or more return data.prices[]. Asking for more than 20 returns 400 with Too many commodity codes requested (max: 20, requested: N). An unrecognised code also returns 400, with a "did you mean" suggestion — so validate your code list once rather than on every poll.
How Often To Poll
This is the section most integrations get wrong, and it is the most common reason a working integration starts returning 402.
How often the data actually changes
Polling faster than this cannot return new information:
| Series | New value roughly every |
|---|---|
BRENT_CRUDE_USD | 2.5 minutes |
GOLD_USD | 3 minutes |
WTI_USD | 5 minutes |
NATURAL_GAS_USD | 5 minutes |
Refined products (DIESEL_USD, GASOLINE_USD) | twice a day |
Nothing we publish moves faster than about every 2.5 minutes. A one-second poll and a two-minute poll return the same number — the first just spends forty times more of your quota to do it.
The interval that fits your plan
Assuming you batch up to 20 codes per request (see above):
| Plan | Quota | Poll every | Requests / day | Series covered |
|---|---|---|---|---|
| Free | 50 / day | 30 minutes | 48 | 20 |
| Developer | 10,000 / month | 5 minutes | 288 | 20 |
| Starter | 50,000 / month | 5 minutes | 1,440 | 100 |
| Professional | 100,000 / month | 5 minutes | 2,880 | 220 |
| Scale | 1,000,000 / month | 5 minutes | 28,800 | 2,000+ |
Two things worth noticing:
The free plan covers 20 series. Fifty requests a day sounds small, and it is not, because each request carries twenty codes. Twenty series refreshed every half hour fits inside the free plan with requests to spare.
Above Developer, extra quota buys additional series rather than a shorter timer. Since nothing updates faster than ~2.5 minutes, a five-minute poll is already at the data's resolution. Spend the allowance on more codes or more endpoints, not on a faster loop.
A default that fits the free plan
Start here. It works on every plan, including free:
import os, time, requests
API_KEY = os.getenv("OILPRICEAPI_KEY")
CODES = "BRENT_CRUDE_USD,WTI_USD,NATURAL_GAS_USD" # up to 20
INTERVAL = 1800 # 30 minutes — fits the free plan (48 requests/day)
# Developer and above: drop to 300 (5 minutes)
_cache = {"data": None, "at": 0}
def latest():
"""Return the most recent prices, refreshing at most once per INTERVAL."""
if _cache["data"] and time.time() - _cache["at"] < INTERVAL:
return _cache["data"]
r = requests.get(
"https://api.oilpriceapi.com/v1/prices/latest",
headers={"Authorization": f"Token {API_KEY}"},
params={"by_code": CODES},
timeout=10,
)
if r.status_code == 402:
# Quota exhausted. Serve the last good value rather than failing, and
# lengthen INTERVAL — do not retry immediately.
return _cache["data"]
r.raise_for_status()
_cache.update(data=r.json()["data"], at=time.time())
return _cache["data"]
const INTERVAL = 30 * 60 * 1000; // 30 minutes — fits the free plan
// Developer and above: 5 * 60 * 1000
let cache = { data: null, at: 0 };
async function latest() {
if (cache.data && Date.now() - cache.at < INTERVAL) return cache.data;
const res = await fetch(
"https://api.oilpriceapi.com/v1/prices/latest?by_code=" +
"BRENT_CRUDE_USD,WTI_USD,NATURAL_GAS_USD",
{ headers: { Authorization: `Token ${process.env.OILPRICEAPI_KEY}` } },
);
if (res.status === 402) return cache.data; // quota spent — serve last good value
if (!res.ok) throw new Error(`OilPriceAPI ${res.status}`);
cache = { data: (await res.json()).data, at: Date.now() };
return cache.data;
}
Series that publish on a schedule
Refined products, inventories and report-driven series publish on a cadence, not continuously. Polling them every five minutes spends quota re-reading yesterday's number.
| Series family | Publishes | Poll |
|---|---|---|
| Spot crude, gas, metals | continuously | every 5 minutes |
| Refined products (diesel, gasoline) | ~twice a day | every 6 hours |
| Weekly inventory and rig counts | weekly | once a day |
| Monthly reports (OPEC, EIA STEO) | monthly | once a day |
# A monthly series does not need a fast timer — one check a day is generous.
SCHEDULES = {
"BRENT_CRUDE_USD": 300, # 5 minutes
"DIESEL_USD": 21600, # 6 hours
"US_RIG_COUNT": 86400, # daily
}
If you are already seeing 402
Lengthen the interval and batch your codes before upgrading. Most integrations that hit the wall are polling several times a minute for data that changes every few minutes — the fix is usually a timer change, not a bigger plan.
Rate Limit Error Responses
429 Too Many Requests
When you exceed rate limits:
{
"error": "Rate limit exceeded",
"error_code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests. Upgrade for higher limits.",
"retry_after": 37
}
Response Headers on 429
HTTP/2 429
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 37
Retry-After: 37
Retry-After and X-RateLimit-Reset are seconds until the window resets, not a timestamp. Sleep for that long and retry.
Best Practices
1. Cache Responses
class CachedAPIClient {
constructor(apiKey, cacheTTL = 300000) {
// 5 minutes default
this.apiKey = apiKey;
this.cache = new Map();
this.cacheTTL = cacheTTL;
}
async fetch(endpoint, params = {}) {
const cacheKey = `${endpoint}:${JSON.stringify(params)}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
console.log("Cache hit");
return cached.data;
}
const response = await fetch(
`https://api.oilpriceapi.com/v1${endpoint}?${new URLSearchParams(params)}`,
{ headers: { Authorization: `Token ${this.apiKey}` } },
);
const data = await response.json();
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
});
return data;
}
}
2. Use Webhooks for Real-time Updates
Instead of polling, use webhooks (available on Starter and above):
# Instead of polling every minute
# ❌ while True:
# data = fetch_prices()
# time.sleep(60)
# ✅ Set up a webhook endpoint
from flask import Flask, request
app = Flask(__name__)
@app.route('/webhook/prices', methods=['POST'])
def handle_price_update():
data = request.json
# Process real-time price update
return '', 200
3. Implement Request Queuing
class RequestQueue {
constructor(apiKey, requestsPerMinute = 100) {
this.apiKey = apiKey;
this.queue = [];
this.interval = 60000 / requestsPerMinute;
this.processing = false;
}
async add(endpoint, params) {
return new Promise((resolve, reject) => {
this.queue.push({ endpoint, params, resolve, reject });
if (!this.processing) {
this.process();
}
});
}
async process() {
this.processing = true;
while (this.queue.length > 0) {
const { endpoint, params, resolve, reject } = this.queue.shift();
try {
const response = await fetch(
`https://api.oilpriceapi.com/v1${endpoint}`,
{
headers: { Authorization: `Token ${this.apiKey}` },
params,
},
);
resolve(await response.json());
} catch (error) {
reject(error);
}
// Wait before next request
await new Promise((r) => setTimeout(r, this.interval));
}
this.processing = false;
}
}
Monitoring Your Usage
Track Usage in Your Application
import logging
from datetime import datetime
class UsageMonitor:
def __init__(self):
self.requests_made = 0
self.monthly_remaining = None
self.rate_limit_remaining = None
def log_response(self, response):
# Extract headers
self.monthly_remaining = response.headers.get('X-Monthly-Remaining')
self.rate_limit_remaining = response.headers.get('X-RateLimit-Remaining')
self.requests_made += 1
# Log if approaching limits
if self.monthly_remaining and int(self.monthly_remaining) < 100:
logging.warning(f"Only {self.monthly_remaining} monthly requests remaining!")
if self.rate_limit_remaining and int(self.rate_limit_remaining) < 10:
logging.warning(f"Only {self.rate_limit_remaining} requests remaining in rate limit window!")
Dashboard Monitoring
Monitor your usage at oilpriceapi.com/dashboard:
- Real-time request count
- Usage by endpoint
- Error rates
- Response times
- Geographic distribution
Upgrading Your Plan
If you consistently hit rate limits, consider upgrading:
- Monitor your usage patterns
- Calculate required limits
- Visit oilpriceapi.com/pricing
- Upgrade instantly without downtime