Your rate limit defines how many API calls you can make per entity per hour, based on your active plan and any API call add-ons youβve purchased.
Rate limits per plan
Each plan includes a fixed number of API calls per entity per hour:
Starter: 2,000 API calls / entity / hour
Pro: 2,500 API calls / entity / hour
Growth: 3,000 API calls / entity / hour
Enterprise: 5,000 API calls / entity / hour
If youβve purchased API call add-ons, these increase your hourly limit accordingly.
Understanding and optimising your API usage is critical for building reliable applications. This guide explains how rate limits work, how to stay within them, and strategies for maximum efficiency.
Quick summary
Default limits:
Limits are per entity, not per endpoint
Resets after 1 hour from first request
Track usage in real-time via response headers
When you hit the limit:
You receive a 429 Too Many Requests error
You can still call other entities
Wait for reset or upgrade your plan
Pro tip: Use includes, cache reference data, and implement smart polling to reduce API calls by 50-80%.
1. How rate limits work
Key points:
β Limits are per entity (Fixture, Team, Player, etc.)
β The hour starts from your first request to that entity
β After 1 hour from the first request, your limit resets
β Different entities have separate limits
Example timeline
What counts as a request?
Each of these counts as ONE request:
GET /fixtures/123
GET /fixtures?date=2026-03-02
GET /fixtures/123?include=participants;events;statistics
Each page in paginated results
These count as SEPARATE requests:
GET /fixtures/123 (1 Fixture request)
GET /teams/456 (1 Team request)
Different entities = different rate limit buckets
2. Understanding entities vs endpoints {#entities-vs-endpoints}
This is crucial: Rate limits are per entity, not per endpoint.
18:18 UTC - First Fixture request β Counter starts at 2,999 remaining
18:30 UTC - 50 more Fixture requests β 2,949 remaining
19:00 UTC - 200 more Fixture requests β 2,749 remaining
19:18 UTC - Limit resets to 3,000 β Full limit restored
// These ALL count toward the SAME Fixture entity limit
await fetch('/fixtures/123'); // Fixture request #1
await fetch('/fixtures/date/2026-03-02'); // Fixture request #2
await fetch('/fixtures/multi/123,456,789'); // Fixture request #3
await fetch('/livescores/inplay'); // Fixture request #4 (livescores use Fixture entity)
// Current Fixture limit usage: 4/3000
// This uses a DIFFERENT limit (Team entity)
await fetch('/teams/53'); // Team request #1
// Fixture: 4/3000, Team: 1/3000
// β Bad: Poll all matches every 5 seconds
setInterval(async () => {
await fetch('/livescores'); // Even pre-match and finished games!
}, 5000);
// β Good: Poll only what's needed
async function smartLivescorePolling() {
// Get only matches that updated in last 10 seconds
const updates = await fetch('/livescores/latest?api_token=YOUR_TOKEN');
// Only update UI for changed matches
updateOnlyChangedMatches(updates.data);
}
// Poll every 10 seconds (not 5)
setInterval(smartLivescorePolling, 10000);
// Or even better - only poll during live matches
function adaptivePolling() {
const hasLiveMatches = checkIfAnyLiveMatches();
if (hasLiveMatches) {
// Poll every 10 seconds during live matches
return setInterval(smartLivescorePolling, 10000);
} else {
// Poll every 5 minutes when no live matches
return setInterval(smartLivescorePolling, 300000);
}
}
// β Slow: Many requests with includes disabled
// Default: 25 per page = 40 requests for 1000 items
for (let page = 1; page <= 40; page++) {
await fetch(`/fixtures?page=${page}`);
}
// β Fast: Fewer requests, more items per page
// With populate: 1000 per page = 1 request for 1000 items
await fetch('/fixtures?filters=populate&per_page=1000');
// Note: includes are disabled with populate filter
Daily API Calls for a typical livescore app:
- 10,000 fixture requests
- 5,000 team requests
- 3,000 type lookups
- 2,000 state lookups
= 20,000 total requests/day
Daily API Calls with all strategies:
- 2,000 fixture requests (includes, batching, smart polling)
- 500 team requests (includes)
- 1 type lookup (cached for 1 week)
- 1 state lookup (cached for 1 week)
= 2,502 total requests/day
{
"error": "Too Many Requests",
"message": "Rate limit of 3000 requests per hour exceeded.",
"retry_after": 1847,
"rate_limit": {
"remaining": 0,
"total": 3000,
"resets_in_seconds": 1847,
"requested_entity": "Fixture"
}
}
import time
import requests
from datetime import datetime, timedelta
from typing import Dict, List, Optional
class RateLimiter:
def __init__(self, max_requests: int = 3000, window_seconds: int = 3600):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests: Dict[str, List[float]] = {}
def throttle(self, entity: str = 'default'):
now = time.time()
if entity not in self.requests:
self.requests[entity] = []
# Remove old requests
self.requests[entity] = [
req_time for req_time in self.requests[entity]
if now - req_time < self.window_seconds
]
# Check if at limit
if len(self.requests[entity]) >= self.max_requests:
oldest = self.requests[entity][0]
wait_time = self.window_seconds - (now - oldest)
print(f"Throttling {entity}: waiting {wait_time:.2f}s")
time.sleep(wait_time)
return self.throttle(entity)
# Add this request
self.requests[entity].append(now)
class SportmonksAPI:
def __init__(self, api_token: str):
self.token = api_token
self.base_url = 'https://api.sportmonks.com/v3/football'
self.cache = {}
self.limiter = RateLimiter(max_requests=2800) # Leave buffer
def request(self, endpoint: str, params: Optional[Dict] = None, entity: str = 'default'):
# Throttle
self.limiter.throttle(entity)
# Build request
url = f"{self.base_url}{endpoint}"
if params is None:
params = {}
params['api_token'] = self.token
# Make request with retry
for attempt in range(3):
try:
response = requests.get(url, params=params, timeout=30)
if response.status_code == 429:
retry_after = response.json().get('retry_after', 2 ** attempt)
print(f"Rate limited. Retrying after {retry_after}s")
time.sleep(retry_after)
continue
response.raise_for_status()
data = response.json()
# Log rate limit
if 'rate_limit' in data:
remaining = data['rate_limit']['remaining']
print(f"{entity}: {remaining} remaining")
return data
except requests.exceptions.RequestException as e:
if attempt == 2:
raise
time.sleep(2 ** attempt)
def get_types(self):
cache_key = 'types'
cache_duration = timedelta(weeks=1)
if cache_key in self.cache:
data, timestamp = self.cache[cache_key]
if datetime.now() - timestamp < cache_duration:
print("Using cached types")
return data
response = self.request('/core/types', entity='Type')
self.cache[cache_key] = (response['data'], datetime.now())
return response['data']
# Usage
api = SportmonksAPI('YOUR_TOKEN')
fixtures = api.request('/fixtures', {
'filters': 'fixtureLeagues:501',
'include': 'participants;scores'
}, 'Fixture')
types = api.get_types() # Cached
<?php
class RateLimiter {
private $maxRequests;
private $windowSeconds;
private $requests = [];
public function __construct($maxRequests = 3000, $windowSeconds = 3600) {
$this->maxRequests = $maxRequests;
$this->windowSeconds = $windowSeconds;
}
public function throttle($entity = 'default') {
$now = time();
// Initialise entity if needed
if (!isset($this->requests[$entity])) {
$this->requests[$entity] = [];
}
// Remove old requests outside the window
$this->requests[$entity] = array_filter(
$this->requests[$entity],
function($timestamp) use ($now) {
return ($now - $timestamp) < $this->windowSeconds;
}
);
// Check if at limit
if (count($this->requests[$entity]) >= $this->maxRequests) {
$oldestRequest = min($this->requests[$entity]);
$waitTime = $this->windowSeconds - ($now - $oldestRequest);
echo "Throttling {$entity}: waiting {$waitTime}s\n";
sleep($waitTime);
// Try again recursively
return $this->throttle($entity);
}
// Add this request to history
$this->requests[$entity][] = $now;
}
}
class SportmonksAPI {
private $token;
private $baseUrl = 'https://api.sportmonks.com/v3/football';
private $cache = [];
private $limiter;
public function __construct($apiToken, $maxRequests = 2800) {
$this->token = $apiToken;
$this->limiter = new RateLimiter($maxRequests);
}
public function request($endpoint, $params = [], $entity = 'default', $maxRetries = 3) {
// Throttle request
$this->limiter->throttle($entity);
// Build URL
$params['api_token'] = $this->token;
$url = $this->baseUrl . $endpoint . '?' . http_build_query($params);
// Make request with retry logic
for ($attempt = 0; $attempt < $maxRetries; $attempt++) {
try {
$response = $this->fetchWithRetry($url, $attempt);
$data = json_decode($response, true);
// Log rate limit info
if (isset($data['rate_limit'])) {
$this->logRateLimit($data['rate_limit']);
}
return $data;
} catch (Exception $e) {
if ($attempt === $maxRetries - 1) {
throw $e;
}
// Exponential backoff
$waitTime = pow(2, $attempt);
echo "Request failed. Retrying after {$waitTime}s...\n";
sleep($waitTime);
}
}
}
private function fetchWithRetry($url, $attempt) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 429) {
$data = json_decode($response, true);
$retryAfter = $data['retry_after'] ?? pow(2, $attempt);
echo "Rate limited. Retrying after {$retryAfter}s...\n";
sleep($retryAfter);
// Retry recursively
return $this->fetchWithRetry($url, $attempt);
}
if ($httpCode !== 200) {
throw new Exception("HTTP {$httpCode}: {$response}");
}
return $response;
}
private function logRateLimit($rateLimit) {
$remaining = $rateLimit['remaining'] ?? '?';
$entity = $rateLimit['requested_entity'] ?? 'Unknown';
echo "{$entity}: {$remaining} requests remaining\n";
if ($remaining < 200) {
echo "β οΈ Low on {$entity} requests! Optimize your calls.\n";
}
}
// Cached method for types
public function getTypes() {
$cacheKey = 'types';
$cacheDuration = 7 * 24 * 60 * 60; // 1 week
if (isset($this->cache[$cacheKey])) {
$cached = $this->cache[$cacheKey];
if (time() - $cached['timestamp'] < $cacheDuration) {
echo "Using cached types\n";
return $cached['data'];
}
}
echo "Fetching fresh types\n";
$response = $this->request('/core/types', [], 'Type');
$this->cache[$cacheKey] = [
'data' => $response['data'],
'timestamp' => time()
];
return $response['data'];
}
// Helper method for batched requests
public function getFixturesByIds($ids) {
if (empty($ids)) {
return [];
}
$idsString = implode(',', $ids);
return $this->request("/fixtures/multi/{$idsString}", [], 'Fixture');
}
}
// Usage Example
try {
$api = new SportmonksAPI('YOUR_TOKEN_HERE');
// Single fixture with includes
$fixture = $api->request('/fixtures/123', [
'include' => 'participants;scores;events'
], 'Fixture');
echo "Fixture: {$fixture['data']['name']}\n";
// Batched request
$multipleFixtures = $api->getFixturesByIds([123, 456, 789]);
echo "Fetched " . count($multipleFixtures['data']) . " fixtures\n";
// Cached types (only fetches once)
$types = $api->getTypes();
echo "Got " . count($types) . " types\n";
// Types from cache (0 API calls)
$typesAgain = $api->getTypes();
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
// Poll smartly - only active matches
async function updateLivescores() {
// Use /livescores/latest - only matches updated in last 10s
const response = await api.request('/livescores/latest', {
include: 'scores;events'
}, 'Fixture');
// Only update changed matches
updateChangedMatches(response.data);
}
// Adaptive polling
let pollInterval = null;
function startPolling() {
const hasLiveMatches = checkLiveMatches();
const interval = hasLiveMatches ? 10000 : 60000; // 10s or 1min
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(updateLivescores, interval);
}
// Use filters=populate for bulk operations
async function populateFixtures(seasonId) {
let hasMore = true;
let page = 1;
while (hasMore) {
const response = await api.request('/fixtures', {
filters: `populate;fixtureSeason:${seasonId}`,
per_page: 1000,
page: page
}, 'Fixture');
// Save to database
await saveToDB(response.data);
hasMore = response.pagination.has_more;
page++;
// Be nice - small delay between pages
await sleep(100);
}
}
// Cache types once, use includes efficiently
const types = await api.getTypes(); // Cached - 0 API calls after first
const typesMap = new Map(types.map(t => [t.id, t]));
// Get fixture with all stats in one call
const fixture = await api.request('/fixtures/123', {
include: 'statistics;participants;scores'
}, 'Fixture');
// Decode types from cache (no API calls)
fixture.statistics.forEach(stat => {
stat.typeName = typesMap.get(stat.type_id).name;
});
β‘ Using includes to combine related data
β‘ Caching types, states, and leagues
β‘ Implemented rate limit monitoring
β‘ Handling 429 errors with retry logic
β‘ Using /livescores/latest instead of /livescores
β‘ Polling at reasonable intervals (10-30s, not 1s)
β‘ Using filters=populate for database population
β‘ Batching requests with multi-ID endpoints
β‘ Logging rate limit info for debugging
β‘ Have fallback for when limits are hit