Polling Best Practices
Polling Best Practices
The RotoWire API is a REST polling API — there are no webhooks or push events. This guide explains how to poll efficiently, avoid redundant work, and handle errors reliably.
Poll Intervals by Data Type
Different data types update at different frequencies. Polling too aggressively wastes quota and slows your app; polling too infrequently means stale data.
| Data Type | Off-Day | Game Day (Pre-Game) | Game Day (In-Game / Post-Game) |
|---|---|---|---|
| Injuries | Every 30–60 min | Every 10–15 min | Every 5 min |
| News & Headlines | Every 30–60 min | Every 10 min | Every 5 min |
| Lineups | Every 60 min | Every 5 min (2 hrs before tip) | Every 10 min |
| Depth Charts | Every 4–6 hrs | Every 60 min | Every 60 min |
| DFS Projections | Once daily | Once after lineup lock | Not applicable |
| Players / Rosters | Every 4–6 hrs | Every 4–6 hrs | Every 4–6 hrs |
| Transactions | Every 30–60 min | Every 30 min | Every 30 min |
Rule of thumb: If data is time-sensitive for lineups or DFS, tighten your pre-game window. If it's roster or depth chart data, daily is usually enough outside of trade deadlines.
Delta-Pull with the hours Parameter
hours ParameterMost news and injury endpoints support an hours parameter. Use it to fetch only updates published within the last N hours — this is dramatically more efficient than fetching the full dataset on every poll.
# Fetch only updates from the last 30 minutes
curl "https://api.rotowire.com/Basketball/get-nba-news-updates.php?key=YOUR_API_KEY&hours=0.5"
# Fetch updates from the last 2 hours
curl "https://api.rotowire.com/Basketball/get-nba-injuries.php?key=YOUR_API_KEY&hours=2"import requests, time
API_KEY = "YOUR_API_KEY"
POLL_INTERVAL_SECONDS = 300 # 5 minutes
while True:
resp = requests.get(
"https://api.rotowire.com/Basketball/get-nba-injuries.php",
params={"key": API_KEY, "hours": 0.1} # last 6 minutes
)
updates = resp.json().get("Updates", [])
for u in updates:
process_update(u)
time.sleep(POLL_INTERVAL_SECONDS)const API_KEY = 'YOUR_API_KEY';
const POLL_MS = 5 * 60 * 1000;
async function poll() {
const resp = await fetch(
`https://api.rotowire.com/Basketball/get-nba-injuries.php?key=${API_KEY}&hours=0.1`
);
const { Updates = [] } = await resp.json();
for (const update of Updates) {
processUpdate(update);
}
}
setInterval(poll, POLL_MS);
poll(); // run immediately on startDeduplication by Update ID
Every update object has a unique Id field. Even when using the hours parameter, the same update can appear across multiple polls if it falls within the time window. Always deduplicate by Id before processing.
seen_ids = set()
def process_updates(updates):
for update in updates:
uid = update["Id"]
if uid in seen_ids:
continue
seen_ids.add(uid)
handle_update(update)const seenIds = new Set();
function processUpdates(updates) {
for (const update of updates) {
if (seenIds.has(update.Id)) continue;
seenIds.add(update.Id);
handleUpdate(update);
}
}Persistence: Store seen IDs in a database (not just in memory) if your process restarts, so you don't reprocess on startup.
Error Handling & Backoff
Network errors and transient 5xx responses are normal for any HTTP API. Never retry immediately — use exponential backoff.
import requests, time
def fetch_with_retry(url, params, max_retries=5):
delay = 5
for attempt in range(max_retries):
try:
resp = requests.get(url, params=params, timeout=10)
if resp.status_code == 200:
return resp.json()
elif resp.status_code == 429:
print(f"Rate limited. Waiting {delay}s...")
elif resp.status_code >= 500:
print(f"Server error {resp.status_code}. Retrying in {delay}s...")
else:
resp.raise_for_status() # 4xx errors are not retryable
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}. Retrying in {delay}s...")
time.sleep(delay)
delay = min(delay * 2, 120) # cap at 2 minutes
raise Exception("Max retries exceeded")async function fetchWithRetry(url, maxRetries = 5) {
let delay = 5000;
for (let i = 0; i < maxRetries; i++) {
try {
const resp = await fetch(url);
if (resp.ok) return await resp.json();
if (resp.status === 429 || resp.status >= 500) {
console.warn(`HTTP ${resp.status} — retrying in ${delay}ms`);
} else {
throw new Error(`Non-retryable error: ${resp.status}`);
}
} catch (e) {
if (i === maxRetries - 1) throw e;
console.warn(`Fetch error: ${e.message} — retrying in ${delay}ms`);
}
await new Promise(r => setTimeout(r, delay));
delay = Math.min(delay * 2, 120000);
}
}Avoiding Redundant Calls
Don't poll all sports at once. Only poll the sports and leagues relevant to your application.
Use date to scope requests on endpoints that support it:
# Only today's data
curl "https://api.rotowire.com/Baseball/get-mlb-injuries.php?key=YOUR_API_KEY&date=2025-04-10"Respect game schedules. There is no NBA data to fetch in July. Build schedule-awareness into your polling loop to pause or reduce frequency during off-seasons.
Cache full-dataset responses. Endpoints like get-nba-players.php or get-nba-depth-charts.php return large payloads that rarely change. Store them locally and refresh infrequently.
import time
_depth_chart_cache = {"data": None, "expires": 0}
def get_depth_charts():
if time.time() < _depth_chart_cache["expires"]:
return _depth_chart_cache["data"]
resp = requests.get("https://api.rotowire.com/Basketball/get-nba-depth-charts.php",
params={"key": API_KEY})
_depth_chart_cache["data"] = resp.json()
_depth_chart_cache["expires"] = time.time() + 3600 # cache for 1 hour
return _depth_chart_cache["data"]Checklist
- Use
hoursparam for news/injury delta-pulls - Deduplicate by
Idfield before processing - Persist seen IDs across process restarts
- Implement exponential backoff on 429 and 5xx
- Set a request timeout (10s recommended)
- Cache slow-changing data (depth charts, rosters)
- Pause polling during off-seasons for inactive leagues
- Never embed your API key in client-side or public code
Updated 2 days ago