The Modrinth API implements rate limiting to ensure fair usage and maintain service quality for all users. Rate limits are applied per IP address using the GCRA (Generic Cell Rate Algorithm).
Rate Limit Configuration
Default Limits
The API enforces the following rate limits:
300 requests per minute per IP address
Burst capacity : 300 requests
Rate limits are the same whether you use an authenticated request (with a token) or an unauthenticated request.
How GCRA Works
Modrinth uses GCRA for smooth rate limiting:
Requests are allowed at a steady rate (300/minute = 5 requests/second)
You can burst up to 300 requests at once
After a burst, you must wait for the rate limit to replenish
The algorithm prevents thundering herd effects
Every API response includes rate limit information in the headers:
X-Ratelimit-Limit : 300
X-Ratelimit-Remaining : 245
X-Ratelimit-Reset : 42
The maximum number of requests allowed per minute (always 300)
The number of requests remaining in the current rate limit window
The time in seconds until the rate limit fully resets
Monitoring Rate Limits
Always check these headers to avoid hitting rate limits:
const response = await fetch ( 'https://api.modrinth.com/v2/search' , {
headers: { 'User-Agent' : 'my-app/1.0.0' }
});
const limit = response . headers . get ( 'X-Ratelimit-Limit' );
const remaining = response . headers . get ( 'X-Ratelimit-Remaining' );
const reset = response . headers . get ( 'X-Ratelimit-Reset' );
console . log ( `Rate limit: ${ remaining } / ${ limit } , resets in ${ reset } s` );
if ( remaining < 10 ) {
console . warn ( 'Approaching rate limit!' );
}
Rate Limit Exceeded
When you exceed the rate limit, the API returns a 429 Too Many Requests response.
429 Response Example
HTTP / 1.1 429 Too Many Requests
Content-Type : application/json
X-Ratelimit-Limit : 300
X-Ratelimit-Remaining : 0
X-Ratelimit-Reset : 15
Retry-After : 15
{
"error" : "ratelimit_error" ,
"description" : "You are being rate-limited. Please wait 15000 milliseconds. 0/300 remaining."
}
The number of seconds to wait before making another request
Will be 0 when rate limited
Seconds until you can make requests again
Handling Rate Limits
Basic Retry Logic
Implement exponential backoff when you receive a 429:
async function fetchWithRetry ( url , options = {}, maxRetries = 3 ) {
for ( let i = 0 ; i < maxRetries ; i ++ ) {
const response = await fetch ( url , {
... options ,
headers: {
'User-Agent' : 'my-app/1.0.0' ,
... options . headers
}
});
if ( response . status !== 429 ) {
return response ;
}
// Rate limited - wait before retrying
const retryAfter = response . headers . get ( 'Retry-After' );
const waitTime = retryAfter ? parseInt ( retryAfter ) * 1000 : Math . pow ( 2 , i ) * 1000 ;
console . log ( `Rate limited. Waiting ${ waitTime } ms before retry...` );
await new Promise ( resolve => setTimeout ( resolve , waitTime ));
}
throw new Error ( 'Max retries exceeded' );
}
// Usage
try {
const response = await fetchWithRetry ( 'https://api.modrinth.com/v2/search' );
const data = await response . json ();
} catch ( error ) {
console . error ( 'Request failed:' , error );
}
Advanced Rate Limit Management
Use a token bucket or queue to manage requests:
class RateLimiter {
constructor ( maxRequests = 300 , perMilliseconds = 60000 ) {
this . maxRequests = maxRequests ;
this . perMilliseconds = perMilliseconds ;
this . requests = [];
}
async waitForToken () {
const now = Date . now ();
// Remove requests older than the time window
this . requests = this . requests . filter (
time => now - time < this . perMilliseconds
);
if ( this . requests . length >= this . maxRequests ) {
const oldestRequest = this . requests [ 0 ];
const waitTime = this . perMilliseconds - ( now - oldestRequest );
console . log ( `Rate limit reached. Waiting ${ waitTime } ms...` );
await new Promise ( resolve => setTimeout ( resolve , waitTime ));
return this . waitForToken ();
}
this . requests . push ( now );
}
async fetch ( url , options = {}) {
await this . waitForToken ();
return fetch ( url , {
... options ,
headers: {
'User-Agent' : 'my-app/1.0.0' ,
... options . headers
}
});
}
}
// Usage
const limiter = new RateLimiter ();
for ( let i = 0 ; i < 1000 ; i ++ ) {
const response = await limiter . fetch ( 'https://api.modrinth.com/v2/search' );
console . log ( `Request ${ i + 1 } completed` );
}
Python Example with Backoff
import requests
import time
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
def create_session ():
session = requests.Session()
# Configure retry strategy for rate limits
retry_strategy = Retry(
total = 5 ,
status_forcelist = [ 429 , 500 , 502 , 503 , 504 ],
allowed_methods = [ "GET" , "POST" , "PUT" , "DELETE" ],
backoff_factor = 2 # 2, 4, 8, 16, 32 seconds
)
adapter = HTTPAdapter( max_retries = retry_strategy)
session.mount( "https://" , adapter)
session.mount( "http://" , adapter)
return session
def make_request ( session , url ):
headers = { 'User-Agent' : 'my-app/1.0.0' }
response = session.get(url, headers = headers)
# Check remaining rate limit
remaining = response.headers.get( 'X-Ratelimit-Remaining' )
if remaining and int (remaining) < 10 :
print ( f "Warning: Only { remaining } requests remaining" )
return response
# Usage
session = create_session()
response = make_request(session, 'https://api.modrinth.com/v2/search' )
print (response.json())
Best Practices
Always check X-Ratelimit-Remaining before making bulk requests:
if ( remaining < requiredRequests ) {
const waitTime = resetSeconds * 1000 ;
await sleep ( waitTime );
}
2. Implement Caching
Cache API responses to reduce the number of requests:
const cache = new Map ();
const CACHE_TTL = 5 * 60 * 1000 ; // 5 minutes
async function fetchWithCache ( url ) {
const cached = cache . get ( url );
if ( cached && Date . now () - cached . timestamp < CACHE_TTL ) {
return cached . data ;
}
const response = await fetch ( url , {
headers: { 'User-Agent' : 'my-app/1.0.0' }
});
const data = await response . json ();
cache . set ( url , { data , timestamp: Date . now () });
return data ;
}
3. Batch Requests
Use bulk endpoints when available:
# Instead of multiple single requests
curl "https://api.modrinth.com/v2/project/fabric-api"
curl "https://api.modrinth.com/v2/project/sodium"
curl "https://api.modrinth.com/v2/project/lithium"
# Use the bulk endpoint
curl "https://api.modrinth.com/v2/projects?ids=[ \" fabric-api \" , \" sodium \" , \" lithium \" ]"
4. Spread Out Requests
Don’t burst all requests at once. Spread them over time:
async function processItems ( items , processFn , requestsPerSecond = 5 ) {
const delay = 1000 / requestsPerSecond ;
for ( const item of items ) {
await processFn ( item );
await new Promise ( resolve => setTimeout ( resolve , delay ));
}
}
// Process 300 items at 5 requests/second (60 seconds total)
await processItems ( projects , async ( project ) => {
const response = await fetch ( `https://api.modrinth.com/v2/project/ ${ project } ` );
return response . json ();
});
5. Use Conditional Requests
Use If-None-Match headers with ETags to avoid downloading unchanged data:
const etags = new Map ();
async function fetchConditional ( url ) {
const headers = { 'User-Agent' : 'my-app/1.0.0' };
const etag = etags . get ( url );
if ( etag ) {
headers [ 'If-None-Match' ] = etag ;
}
const response = await fetch ( url , { headers });
if ( response . status === 304 ) {
console . log ( 'Data unchanged, using cached version' );
return null ; // Use cached data
}
// Store new ETag
const newEtag = response . headers . get ( 'ETag' );
if ( newEtag ) {
etags . set ( url , newEtag );
}
return response . json ();
}
6. Monitor Your Usage
Track your API usage over time:
class UsageMonitor {
constructor () {
this . requests = [];
this . rateLimitHits = 0 ;
}
recordRequest ( response ) {
this . requests . push ({
timestamp: Date . now (),
status: response . status ,
remaining: response . headers . get ( 'X-Ratelimit-Remaining' )
});
if ( response . status === 429 ) {
this . rateLimitHits ++ ;
}
}
getStats () {
const now = Date . now ();
const lastMinute = this . requests . filter (
r => now - r . timestamp < 60000
);
return {
totalRequests: this . requests . length ,
requestsLastMinute: lastMinute . length ,
rateLimitHits: this . rateLimitHits
};
}
}
Rate Limit Exemptions
Internal Rate Limit Key
Modrinth provides a special rate limit bypass key for internal services. This is not available to public API users.
Do not attempt to use or guess the internal rate limit key. Unauthorized use may result in your IP being blocked.
Requesting Higher Limits
If you have a legitimate use case requiring higher rate limits:
Document your use case : Explain why you need higher limits
Show optimization efforts : Demonstrate you’ve implemented caching and batching
Provide contact information : Include your project details and contact info
Contact Modrinth : Email [email protected]
Higher rate limits are granted on a case-by-case basis for projects that benefit the Modrinth ecosystem.
Cloudflare Integration
Modrinth uses Cloudflare for DDoS protection. The API checks for the CF-Connecting-IP header when Cloudflare integration is enabled.
IP Detection
Rate limits are applied based on:
CF-Connecting-IP header (if Cloudflare is enabled)
Request peer address (fallback)
Using a proxy or VPN may cause you to share a rate limit with other users on the same IP address.
Troubleshooting
Issue: Rate Limited Despite Low Usage
Possible causes:
Sharing an IP with other users (VPN, corporate network, cloud provider)
Multiple applications using the same IP
Burst of requests exceeding the limit
Solutions:
Implement request spreading (5 requests/second max)
Use caching to reduce requests
Contact Modrinth if you have a dedicated IP and still face issues
Possible causes:
Redis connectivity issues (API falls back to reduced limits)
Multiple API servers with slightly different state
Solutions:
Don’t rely on exact values, use headers as guidance
Implement retry logic for all requests
Issue: 401 Instead of 429
Cause:
If the API cannot determine your IP address, you’ll receive a 401 error:
{
"error" : "unauthorized" ,
"description" : "Unable to obtain user IP address!"
}
Solution:
Check your network configuration
Ensure you’re not blocking necessary headers
Contact support if the issue persists
Summary
Current Limit 300 requests per minute per IP address with burst capacity
Headers Monitor X-Ratelimit-Remaining and X-Ratelimit-Reset headers
429 Response Implement exponential backoff using the Retry-After header
Best Practices Cache responses, batch requests, and spread traffic over time
Next Steps
Error Handling Learn about all API error codes and responses
API Overview Return to the API overview page