Rate Limiting
The OEMSTREAM Webhook API limits how many requests you can make to keep the system running smoothly for everyone. This guide shows you how rate limiting works and how to handle it in your code.
How Rate Limiting Works
We track your API usage by your API key. The limits are designed to be fair - high enough for normal use but strict enough to prevent abuse.
Current Limits
| What's Limited | Limit | Time Window | Description |
|---|---|---|---|
| Requests per minute | 100 | 1 minute | Maximum requests per minute per API key |
| Requests per hour | 1,000 | 1 hour | Maximum requests per hour per API key |
| Requests per day | 10,000 | 24 hours | Maximum requests per day per API key |
| Request size | 1 MB | Per request | Maximum size of request body |
| Concurrent requests | 10 | At the same time | Maximum simultaneous requests per API key |
Your rate limits may be different based on your plan. Contact support if you need higher limits.
Rate Limit Headers
Every API response includes headers that tell you about your current rate limit status:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1641859200
X-RateLimit-Window: 60
Retry-After: 30
What Each Header Means
| Header | What It Tells You |
|---|---|
X-RateLimit-Limit | How many requests you can make in this time window |
X-RateLimit-Remaining | How many requests you have left in this time window |
X-RateLimit-Reset | When your rate limit resets (Unix timestamp) |
X-RateLimit-Window | How long the rate limit window is (in seconds) |
Retry-After | How long to wait before trying again (only when rate limited) |
What Happens When You Hit the Limit
When you make too many requests, you'll get a 429 Too Many Requests response:
{
"error": "rate_limit_exceeded",
"message": "Too many requests. Please try again later.",
"retry_after": 60,
"details": {
"limit": 100,
"window": "1 minute",
"reset_time": "2025-01-10T23:00:00Z",
"current_usage": 100
}
}
How to Handle Rate Limits
1. Check Rate Limit Headers
Always look at the rate limit headers in API responses to avoid hitting limits:
class RateLimitAwareClient {
constructor(apiKey, baseUrl) {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
this.rateLimitInfo = {
limit: null,
remaining: null,
reset: null,
window: null
};
}
async sendWebhook(integrationKey, data) {
// Check if we're close to the rate limit
if (this.rateLimitInfo.remaining !== null && this.rateLimitInfo.remaining < 10) {
const waitTime = this.calculateWaitTime();
if (waitTime > 0) {
console.log(`Rate limit approaching. Waiting ${waitTime}ms`);
await this.sleep(waitTime);
}
}
try {
const response = await fetch(`${this.baseUrl}/api/webhooks/${integrationKey}`, {
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
// Update rate limit info from headers
this.updateRateLimitInfo(response.headers);
if (response.status === 429) {
const errorData = await response.json();
throw new RateLimitError(errorData.retry_after);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error instanceof RateLimitError) {
console.log(`Rate limited. Waiting ${error.retryAfter} seconds`);
await this.sleep(error.retryAfter * 1000);
return this.sendWebhook(integrationKey, data); // Retry
}
throw error;
}
}
updateRateLimitInfo(headers) {
this.rateLimitInfo = {
limit: parseInt(headers.get('X-RateLimit-Limit')) || null,
remaining: parseInt(headers.get('X-RateLimit-Remaining')) || null,
reset: parseInt(headers.get('X-RateLimit-Reset')) || null,
window: parseInt(headers.get('X-RateLimit-Window')) || null
};
}
calculateWaitTime() {
if (!this.rateLimitInfo.reset) return 0;
const now = Math.floor(Date.now() / 1000);
const resetTime = this.rateLimitInfo.reset;
return Math.max(0, (resetTime - now) * 1000);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
class RateLimitError extends Error {
constructor(retryAfter) {
super('Rate limit exceeded');
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
2. Implement Exponential Backoff
Use exponential backoff when you encounter rate limits:
import time
import random
from typing import Optional
class RateLimitHandler:
def __init__(self, max_retries: int = 5, base_delay: float = 1.0):
self.max_retries = max_retries
self.base_delay = base_delay
def send_with_backoff(self, send_function, *args, **kwargs):
"""Send webhook with exponential backoff on rate limits"""
for attempt in range(self.max_retries + 1):
try:
return send_function(*args, **kwargs)
except RateLimitError as e:
if attempt >= self.max_retries:
raise Exception(f"Max retries ({self.max_retries}) exceeded")
# Use server-provided retry_after if available
if hasattr(e, 'retry_after') and e.retry_after:
delay = e.retry_after
else:
# Calculate exponential backoff with jitter
delay = self.base_delay * (2 ** attempt)
jitter = random.uniform(0, 0.1) * delay
delay += jitter
print(f"Rate limited. Retrying in {delay:.2f} seconds (attempt {attempt + 1})")
time.sleep(delay)
except Exception as e:
# Don't retry non-rate-limit errors
raise
class WebhookClient:
def __init__(self, api_key: str, base_url: str):
self.api_key = api_key
self.base_url = base_url
self.rate_limit_handler = RateLimitHandler()
def send_webhook(self, integration_key: str, data: dict) -> dict:
"""Send webhook with automatic rate limit handling"""
return self.rate_limit_handler.send_with_backoff(
self._send_webhook_raw, integration_key, data
)
def _send_webhook_raw(self, integration_key: str, data: dict) -> dict:
"""Raw webhook sending without retry logic"""
# Implementation here...
pass
3. Request Queuing and Throttling
Implement a queue system to manage high-volume webhook sending:
<?php
class WebhookQueue
{
private $queue = [];
private $rateLimiter;
private $isProcessing = false;
public function __construct(RateLimiter $rateLimiter)
{
$this->rateLimiter = $rateLimiter;
}
public function enqueue(string $integrationKey, array $data, int $priority = 0): string
{
$id = uniqid();
$this->queue[] = [
'id' => $id,
'integration_key' => $integrationKey,
'data' => $data,
'priority' => $priority,
'created_at' => time(),
'attempts' => 0
];
// Sort by priority (higher priority first)
usort($this->queue, function ($a, $b) {
return $b['priority'] <=> $a['priority'];
});
$this->processQueue();
return $id;
}
private function processQueue(): void
{
if ($this->isProcessing || empty($this->queue)) {
return;
}
$this->isProcessing = true;
while (!empty($this->queue)) {
$item = array_shift($this->queue);
try {
// Check rate limit before sending
if (!$this->rateLimiter->canMakeRequest()) {
$waitTime = $this->rateLimiter->getWaitTime();
\Log::info("Rate limit reached. Waiting {$waitTime} seconds");
// Put item back at front of queue
array_unshift($this->queue, $item);
// Schedule processing after wait time
$this->scheduleProcessing($waitTime);
break;
}
// Send webhook
$result = $this->sendWebhook($item['integration_key'], $item['data']);
\Log::info('Webhook sent from queue', [
'id' => $item['id'],
'uuid' => $result['uuid']
]);
} catch (RateLimitException $e) {
// Rate limited - put back in queue and wait
$item['attempts']++;
if ($item['attempts'] < 5) {
array_unshift($this->queue, $item);
$this->scheduleProcessing($e->getRetryAfter());
} else {
\Log::error('Webhook failed after max attempts', ['id' => $item['id']]);
}
break;
} catch (\Exception $e) {
\Log::error('Webhook failed', [
'id' => $item['id'],
'error' => $e->getMessage()
]);
}
// Small delay between requests to be respectful
usleep(100000); // 100ms
}
$this->isProcessing = false;
}
private function scheduleProcessing(int $delaySeconds): void
{
// In a real implementation, you'd use a job queue like Redis or database
// For this example, we'll use a simple approach
\Illuminate\Support\Facades\Cache::put(
'webhook_queue_next_process',
time() + $delaySeconds,
$delaySeconds + 60
);
}
}
class RateLimiter
{
private $requests = [];
private $limit;
private $window;
public function __construct(int $limit = 100, int $window = 60)
{
$this->limit = $limit;
$this->window = $window;
}
public function canMakeRequest(): bool
{
$this->cleanOldRequests();
return count($this->requests) < $this->limit;
}
public function recordRequest(): void
{
$this->requests[] = time();
}
public function getWaitTime(): int
{
if (empty($this->requests)) {
return 0;
}
$oldestRequest = min($this->requests);
$windowEnd = $oldestRequest + $this->window;
return max(0, $windowEnd - time());
}
private function cleanOldRequests(): void
{
$cutoff = time() - $this->window;
$this->requests = array_filter($this->requests, function ($timestamp) use ($cutoff) {
return $timestamp > $cutoff;
});
}
}
4. Batch Processing
For high-volume scenarios, consider batching your webhooks:
public class WebhookBatchProcessor
{
private readonly HttpClient _httpClient;
private readonly string _apiKey;
private readonly string _baseUrl;
private readonly SemaphoreSlim _semaphore;
private readonly List<WebhookRequest> _batch;
private readonly Timer _batchTimer;
public WebhookBatchProcessor(string apiKey, string baseUrl, int maxConcurrency = 10)
{
_apiKey = apiKey;
_baseUrl = baseUrl;
_semaphore = new SemaphoreSlim(maxConcurrency);
_batch = new List<WebhookRequest>();
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKey);
// Process batch every 5 seconds or when it reaches 100 items
_batchTimer = new Timer(ProcessBatch, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
}
public async Task<string> QueueWebhookAsync(string integrationKey, object data)
{
var request = new WebhookRequest
{
Id = Guid.NewGuid().ToString(),
IntegrationKey = integrationKey,
Data = data,
QueuedAt = DateTime.UtcNow
};
lock (_batch)
{
_batch.Add(request);
// Process immediately if batch is full
if (_batch.Count >= 100)
{
Task.Run(() => ProcessBatch(null));
}
}
return request.Id;
}
private async void ProcessBatch(object state)
{
List<WebhookRequest> currentBatch;
lock (_batch)
{
if (_batch.Count == 0) return;
currentBatch = new List<WebhookRequest>(_batch);
_batch.Clear();
}
// Process webhooks with controlled concurrency
var tasks = currentBatch.Select(ProcessWebhookWithRateLimit);
await Task.WhenAll(tasks);
}
private async Task ProcessWebhookWithRateLimit(WebhookRequest request)
{
await _semaphore.WaitAsync();
try
{
await ProcessSingleWebhook(request);
}
finally
{
_semaphore.Release();
}
}
private async Task ProcessSingleWebhook(WebhookRequest request)
{
const int maxRetries = 3;
var delay = TimeSpan.FromSeconds(1);
for (int attempt = 0; attempt < maxRetries; attempt++)
{
try
{
var json = JsonSerializer.Serialize(request.Data);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync(
$"{_baseUrl}/api/webhooks/{request.IntegrationKey}",
content
);
if (response.StatusCode == HttpStatusCode.TooManyRequests)
{
// Extract retry-after header
var retryAfter = response.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(60);
await Task.Delay(retryAfter);
continue;
}
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Webhook {request.Id} sent successfully");
return;
}
catch (HttpRequestException ex) when (attempt < maxRetries - 1)
{
Console.WriteLine($"Webhook {request.Id} failed (attempt {attempt + 1}): {ex.Message}");
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff
}
}
Console.WriteLine($"Webhook {request.Id} failed after {maxRetries} attempts");
}
}
public class WebhookRequest
{
public string Id { get; set; }
public string IntegrationKey { get; set; }
public object Data { get; set; }
public DateTime QueuedAt { get; set; }
}
Best Practices
1. Stay Within Rate Limits
- Check headers: Always look at rate limit headers in responses
- Wait when needed: Use exponential backoff when you hit limits
- Use queues: Queue requests for high-volume scenarios
- Batch requests: Group related webhooks together when possible
2. Make Requests Efficiently
// Good: Spread requests over time
const sendWebhooksGradually = async (webhooks) => {
const delay = 60000 / 100; // Spread 100 requests over 1 minute
for (const webhook of webhooks) {
await sendWebhook(webhook.integrationKey, webhook.data);
await sleep(delay);
}
};
// Bad: Send all requests at once
const sendWebhooksBurst = async (webhooks) => {
const promises = webhooks.map(webhook =>
sendWebhook(webhook.integrationKey, webhook.data)
);
await Promise.all(promises); // This will hit rate limits
};
3. Monitor Your Usage
Set up monitoring to track your rate limit usage:
class RateLimitMonitor:
def __init__(self, alert_threshold=0.8):
self.alert_threshold = alert_threshold
self.metrics = []
def record_request(self, rate_limit_remaining, rate_limit_limit):
usage_ratio = 1 - (rate_limit_remaining / rate_limit_limit)
self.metrics.append({
'timestamp': time.time(),
'usage_ratio': usage_ratio,
'remaining': rate_limit_remaining,
'limit': rate_limit_limit
})
if usage_ratio > self.alert_threshold:
self.send_alert(f"Rate limit usage at {usage_ratio:.1%}")
def send_alert(self, message):
# Send alert to monitoring system
print(f"ALERT: {message}")
# In production, send to Slack, email, or monitoring service
4. Need Higher Limits?
If you need to make more requests:
- Contact support for custom limits based on what you're building
- Use caching to avoid making the same requests repeatedly
- Send only what you need - don't include unnecessary data
- Upgrade your plan for higher limits
Troubleshooting
Common Problems
- Suddenly hitting limits: Check for loops or webhooks calling each other
- Limits seem wrong: Make sure you're reading the right headers
- Requests backing up: Add proper error handling and backup queues
- Slow performance: Monitor your usage and spread out requests
Need Help?
If you're having rate limit problems:
- Check your usage in the admin panel
- Look for ways to optimize your request patterns
- Contact support if you need higher limits
- Consider changing your architecture for high-volume use
What's Next
- Explore SDK Examples (coming soon)
- Learn about Error Handling
- View Integration Management through the admin panel
- Return to API Reference