Rate Limiting
Clemta APIs implement rate limiting to ensure fair usage and system stability. This page explains how rate limiting works across all our APIs.Rate Limit Types
Clemta APIs employ different rate limiting strategies depending on the endpoint:- API Key-based Rate Limiting: Applied to most endpoints
- Resource-based Rate Limiting: Applied to specific resources (e.g., per company ID)
Rate Limit Headers
All API responses include headers that provide information about rate limits:Common Headers
string
Displays your API key’s rate limit (e.g., “100 requests per minute”)
integer
Number of requests remaining in the current time window
integer
Maximum number of requests allowed per minute
integer
Unix timestamp when the rate limit resets
string
Indicates which rate limit type is being applied (“api_key” or “resource”)
integer
Seconds to wait before making another request (typically 60 seconds)
Rate Limit Errors
When a rate limit is exceeded, the API will respond with:- HTTP Status Code:
429 Too Many Requests - Error Code:
RATE_LIMIT_EXCEEDED - Error Message: “Rate limit exceeded”
{
"success": false,
"message": "Rate limit exceeded",
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"details": "Too many requests. Please try again later."
}
}
Best Practices
1
Implement exponential backoff
Use a retry strategy with exponential backoff when you receive 429.
2
Monitor your usage
Track
X-RateLimit-Remaining and slow down before hitting limits.3
Use conditional requests
Use
If-Modified-Since for GET endpoints.4
Use pagination
Paginate list endpoints to reduce load.
1. Implement Exponential Backoff
exponential_backoff.go
package main
import (
"fmt"
"math"
"net/http"
"strconv"
"time"
)
func makeRequestWithRetry(url string, apiKey string, maxRetries int) (*http.Response, error) {
client := &http.Client{}
for attempt := 0; attempt < maxRetries; attempt++ {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-KEY", apiKey)
resp, err := client.Do(req)
if err != nil {
if attempt == maxRetries-1 {
return nil, err
}
continue
}
if resp.StatusCode == http.StatusTooManyRequests { // 429
retryAfter := resp.Header.Get("Retry-After")
var delay time.Duration
if retryAfter != "" {
if seconds, err := strconv.Atoi(retryAfter); err == nil {
delay = time.Duration(seconds) * time.Second
}
}
if delay == 0 {
ms := math.Min(1000*math.Pow(2, float64(attempt)), 60000)
delay = time.Duration(ms) * time.Millisecond
}
fmt.Printf("Rate limited. Waiting %v before retry %d\n", delay, attempt+1)
time.Sleep(delay)
continue
}
return resp, nil
}
return nil, fmt.Errorf("max retries exceeded")
}
export async function makeRequestWithRetry(
url: string,
options: RequestInit,
maxRetries: number = 3
): Promise<Response> {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = response.headers.get("Retry-After");
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.min(1000 * Math.pow(2, attempt), 60_000);
console.log(
`Rate limited. Waiting ${delay}ms before retry ${attempt + 1}`
);
await new Promise((resolve) => setTimeout(resolve, delay));
attempt += 1;
continue;
}
return response;
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
attempt += 1;
}
}
throw new Error("Max retries exceeded");
}
// Usage
await makeRequestWithRetry("https://api.clemta.com/formations/123", {
headers: { "X-API-KEY": "your_api_key" },
});
use reqwest::Client;
use std::time::Duration;
use tokio::time::sleep;
pub async fn make_request_with_retry(
url: &str,
api_key: &str,
max_retries: u32,
) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
let client = Client::new();
let mut attempt: u32 = 0;
while attempt < max_retries {
let response = client
.get(url)
.header("X-API-KEY", api_key)
.send()
.await?;
if response.status() == 429 {
let retry_after = response
.headers()
.get("Retry-After")
.and_then(|h| h.to_str().ok())
.and_then(|s| s.parse::<u64>().ok());
let delay = retry_after
.map(Duration::from_secs)
.unwrap_or_else(|| {
let ms = (1000.0_f64 * 2.0_f64.powi(attempt as i32)).min(60000.0);
Duration::from_millis(ms as u64)
});
println!(
"Rate limited. Waiting {:?} before retry {}",
delay,
attempt + 1
);
sleep(delay).await;
attempt += 1;
continue;
}
return Ok(response);
}
Err("Max retries exceeded".into())
}
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.Optional;
public class ExponentialBackoff {
private static final HttpClient client = HttpClient.newHttpClient();
public static HttpResponse<String> makeRequestWithRetry(
String url, String apiKey, int maxRetries) throws IOException, InterruptedException {
for (int attempt = 0; attempt < maxRetries; attempt++) {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-API-KEY", apiKey)
.GET()
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 429) {
Optional<String> retryAfter = res.headers().firstValue("Retry-After");
long delayMs = retryAfter
.map(s -> Long.parseLong(s) * 1000)
.orElse(Math.min((long) Math.pow(2, attempt) * 1000L, 60_000L));
System.out.printf("Rate limited. Waiting %dms before retry %d%n", delayMs, attempt + 1);
Thread.sleep(delayMs);
continue;
}
return res;
}
throw new RuntimeException("Max retries exceeded");
}
}
import asyncio
import math
from typing import Optional
import aiohttp
async def make_request_with_retry(
url: str, api_key: str, max_retries: int = 3
) -> aiohttp.ClientResponse:
async with aiohttp.ClientSession() as session:
for attempt in range(max_retries):
try:
headers = {"X-API-KEY": api_key}
async with session.get(url, headers=headers) as resp:
if resp.status == 429:
retry_after = resp.headers.get("Retry-After")
delay_ms: Optional[float] = (
int(retry_after) * 1000 if retry_after else None
)
if delay_ms is None:
delay_ms = min(1000 * math.pow(2, attempt), 60000)
print(
f"Rate limited. Waiting {int(delay_ms)}ms before retry {attempt + 1}"
)
await asyncio.sleep(delay_ms / 1000)
continue
return resp
except Exception:
if attempt == max_retries - 1:
raise
raise Exception("Max retries exceeded")
2. Monitor Rate Limit Headers
monitor_headers.go
package main
import (
"fmt"
"net/http"
"strconv"
"time"
)
func makeRequestWithMonitoring(url string, apiKey string) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-KEY", apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
remaining := resp.Header.Get("X-RateLimit-Remaining")
reset := resp.Header.Get("X-RateLimit-Reset")
limit := resp.Header.Get("X-RateLimit-Limit")
fmt.Printf("Rate limit: %s/%s remaining\n", remaining, limit)
if remaining != "" && reset != "" {
if r, err := strconv.Atoi(remaining); err == nil && r < 10 {
if rs, err := strconv.ParseInt(reset, 10, 64); err == nil {
wait := (rs * 1000) - time.Now().UnixMilli()
if wait > 0 {
fmt.Printf("Waiting %dms before next request\n", wait)
time.Sleep(time.Duration(wait) * time.Millisecond)
}
}
}
}
return resp, nil
}
export async function makeRequestWithMonitoring(
url: string,
options: RequestInit
): Promise<Response> {
const response = await fetch(url, options);
const remaining = response.headers.get("X-RateLimit-Remaining");
const reset = response.headers.get("X-RateLimit-Reset");
const limit = response.headers.get("X-RateLimit-Limit");
console.log(`Rate limit: ${remaining}/${limit} remaining`);
if (remaining && reset) {
const r = parseInt(remaining, 10);
if (r < 10) {
const waitTime = parseInt(reset, 10) * 1000 - Date.now();
if (waitTime > 0) {
console.log(`Waiting ${waitTime}ms before next request`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
}
return response;
}
use reqwest::Client;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub async fn make_request_with_monitoring(
url: &str,
api_key: &str,
) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
let client = Client::new();
let response = client
.get(url)
.header("X-API-KEY", api_key)
.send()
.await?;
let remaining = response
.headers()
.get("X-RateLimit-Remaining")
.and_then(|h| h.to_str().ok());
let reset = response
.headers()
.get("X-RateLimit-Reset")
.and_then(|h| h.to_str().ok());
let limit = response
.headers()
.get("X-RateLimit-Limit")
.and_then(|h| h.to_str().ok());
println!(
"Rate limit: {}/{} remaining",
remaining.unwrap_or("?"),
limit.unwrap_or("?")
);
if let (Some(r), Some(rs)) = (remaining, reset) {
if let (Ok(ri), Ok(rsi)) = (r.parse::<i32>(), rs.parse::<u64>()) {
if ri < 10 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let wait = (rsi * 1000).saturating_sub(now);
if wait > 0 {
println!("Waiting {}ms before next request", wait);
tokio::time::sleep(Duration::from_millis(wait)).await;
}
}
}
}
Ok(response)
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.time.Instant;
public class MonitorHeaders {
private static final HttpClient client = HttpClient.newHttpClient();
public static HttpResponse<String> makeRequestWithMonitoring(
String url, String apiKey) throws Exception {
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-API-KEY", apiKey)
.GET()
.build();
HttpResponse<String> res = client.send(req, HttpResponse.BodyHandlers.ofString());
String remaining = res.headers().firstValue("X-RateLimit-Remaining").orElse("?");
String reset = res.headers().firstValue("X-RateLimit-Reset").orElse("?");
String limit = res.headers().firstValue("X-RateLimit-Limit").orElse("?");
System.out.printf("Rate limit: %s/%s remaining%n", remaining, limit);
if (!"?".equals(remaining) && !"?".equals(reset)) {
int ri = Integer.parseInt(remaining);
if (ri < 10) {
long wait = (Long.parseLong(reset) * 1000) - Instant.now().toEpochMilli();
if (wait > 0) {
System.out.printf("Waiting %dms before next request%n", wait);
Thread.sleep(wait);
}
}
}
return res;
}
}
import asyncio
import time
import aiohttp
async def make_request_with_monitoring(url: str, api_key: str):
async with aiohttp.ClientSession() as session:
headers = {"X-API-KEY": api_key}
async with session.get(url, headers=headers) as resp:
remaining = resp.headers.get("X-RateLimit-Remaining", "?")
reset = resp.headers.get("X-RateLimit-Reset", "?")
limit = resp.headers.get("X-RateLimit-Limit", "?")
print(f"Rate limit: {remaining}/{limit} remaining")
if remaining != "?" and reset != "?":
if int(remaining) < 10:
wait = (int(reset) * 1000) - int(time.time() * 1000)
if wait > 0:
print(f"Waiting {wait}ms before next request")
await asyncio.sleep(wait / 1000)
return resp
3. Use Conditional Requests
conditional_requests.go
package main
import (
"fmt"
"net/http"
)
func makeConditionalRequest(url string, apiKey string, lastModified string) (*http.Response, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-KEY", apiKey)
if lastModified != "" {
req.Header.Set("If-Modified-Since", lastModified)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode == http.StatusNotModified { // 304
fmt.Println("No changes since last request")
} else {
fmt.Printf("Last-Modified: %s\n", resp.Header.Get("Last-Modified"))
}
return resp, nil
}
export async function makeConditionalRequest(
url: string,
apiKey: string
): Promise<Response> {
const lastModified = localStorage.getItem("lastModified");
const headers: Record<string, string> = { "X-API-KEY": apiKey };
if (lastModified) headers["If-Modified-Since"] = lastModified;
const response = await fetch(url, { headers });
if (response.status === 304) {
console.log("No changes since last request");
} else {
const newLastModified = response.headers.get("Last-Modified");
if (newLastModified) localStorage.setItem("Last-Modified", newLastModified);
}
return response;
}
use reqwest::header::HeaderMap;
use reqwest::Client;
pub async fn make_conditional_request(
url: &str,
api_key: &str,
last_modified: Option<&str>,
) -> Result<reqwest::Response, Box<dyn std::error::Error>> {
let client = Client::new();
let mut headers = HeaderMap::new();
headers.insert("X-API-KEY", api_key.parse()?);
if let Some(lm) = last_modified {
headers.insert("If-Modified-Since", lm.parse()?);
}
let res = client.get(url).headers(headers).send().await?;
if res.status() == 304 {
println!("No changes since last request");
} else if let Some(lm) = res.headers().get("Last-Modified") {
println!("Last-Modified: {}", lm.to_str()?);
}
Ok(res)
}
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.URI;
import java.util.Optional;
public class ConditionalRequests {
private static final HttpClient client = HttpClient.newHttpClient();
public static HttpResponse<String> makeConditionalRequest(
String url, String apiKey, Optional<String> lastModified) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("X-API-KEY", apiKey)
.GET();
lastModified.ifPresent(lm -> b.header("If-Modified-Since", lm));
HttpResponse<String> res = client.send(b.build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 304) {
System.out.println("No changes since last request");
} else {
res.headers().firstValue("Last-Modified")
.ifPresent(lm -> System.out.println("Last-Modified: " + lm));
}
return res;
}
}
import aiohttp
from typing import Optional
async def make_conditional_request(
url: str, api_key: str, last_modified: Optional[str] = None
):
async with aiohttp.ClientSession() as session:
headers = {"X-API-KEY": api_key}
if last_modified:
headers["If-Modified-Since"] = last_modified
async with session.get(url, headers=headers) as resp:
if resp.status == 304:
print("No changes since last request")
else:
lm = resp.headers.get("Last-Modified")
if lm:
print(f"Last-Modified: {lm}")
return resp
Example Rate Limit Headers
Successful Request
X-RateLimit-API: 100 requests per minute
X-RateLimit-Remaining: 95
X-RateLimit-Limit: 100
X-RateLimit-Reset: 1640995200
X-RateLimit-Type: api_key
Rate Limited Request
X-RateLimit-API: 100 requests per minute
X-RateLimit-Remaining: 0
X-RateLimit-Limit: 100
X-RateLimit-Reset: 1640995200
X-RateLimit-Type: api_key
Retry-After: 60