Rate Limiting & Best Practices
Understand API rate limits and optimize your integration for performance and reliability
Learn how to work within rate limits and build efficient, reliable integrations with the Coherence API.
Rate Limiting Overview
Rate limiting protects the API from abuse and ensures fair usage across all customers. Requests that exceed limits receive a 429 Too Many Requests response.
Rate Limit Tiers
Limits vary by subscription plan:
| Plan | Requests/minute | Requests/day |
|---|---|---|
| Free | 100 | 1,000 |
| Pro | 1,000 | 50,000 |
| Enterprise | 5,000 | Unlimited |
Need higher limits? Contact sales to discuss Enterprise plans with custom rate limits.
Rate Limit Headers
Every API response includes headers with your current rate limit status:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1706745600
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when the limit resets |
Handling 429 Too Many Requests
When you exceed the rate limit, the API returns:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Response body:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please retry after 30 seconds.",
"retry_after": 30
}
}Retry Strategies
Exponential Backoff
Implement exponential backoff to handle rate limits gracefully:
async function fetchWithRetry(
url: string,
options: RequestInit,
maxRetries = 5
): Promise<Response> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.status === 429) {
const retryAfter = parseInt(
response.headers.get('Retry-After') || '1',
10
);
const backoffTime = Math.max(
retryAfter * 1000,
Math.pow(2, attempt) * 1000
);
console.log(`Rate limited. Retrying in ${backoffTime}ms...`);
await sleep(backoffTime);
continue;
}
return response;
} catch (error) {
lastError = error as Error;
const backoffTime = Math.pow(2, attempt) * 1000;
await sleep(backoffTime);
}
}
throw lastError || new Error('Max retries exceeded');
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}Rate Limit Tracking
Monitor your rate limit usage proactively:
class RateLimitTracker {
private remaining: number = Infinity;
private resetTime: number = 0;
updateFromResponse(response: Response): void {
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');
if (remaining) this.remaining = parseInt(remaining, 10);
if (reset) this.resetTime = parseInt(reset, 10) * 1000;
}
async waitIfNeeded(): Promise<void> {
if (this.remaining <= 5) {
const waitTime = this.resetTime - Date.now();
if (waitTime > 0) {
console.log(`Approaching rate limit. Waiting ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
canMakeRequest(): boolean {
return this.remaining > 0 || Date.now() > this.resetTime;
}
}Best Practices
Efficient API Usage
Use Bulk Endpoints
When creating or updating multiple records, use bulk endpoints to reduce API calls:
// Inefficient: 100 API calls
for (const contact of contacts) {
await api.post('/modules/contacts/records', contact);
}
// Efficient: 1 API call
await api.post('/modules/contacts/records/bulk', {
records: contacts
});Bulk endpoints accept up to 100 records per request.
Request Only Needed Fields
Use the fields parameter to fetch only the data you need:
# Fetch only name and email (faster response)
curl "https://api.getcoherence.io/v1/modules/contacts/records?fields=name,email" \
-H "Authorization: Bearer YOUR_API_KEY"Cache Responses
Cache data that doesn't change frequently:
import NodeCache from 'node-cache';
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
async function getModuleSchema(moduleSlug: string) {
const cacheKey = `schema:${moduleSlug}`;
let schema = cache.get(cacheKey);
if (schema) return schema;
const response = await api.get(`/modules/${moduleSlug}`);
schema = response.data;
cache.set(cacheKey, schema);
return schema;
}Module schemas and field configurations rarely change. Cache these for at least 5 minutes.
Pagination Best Practices
Use Cursor Pagination for Large Datasets
Cursor pagination is more reliable than offset pagination for large or frequently-changing datasets:
async function fetchAllRecords(moduleSlug: string) {
const allRecords = [];
let cursor: string | null = null;
do {
const params = new URLSearchParams({
per_page: '100',
...(cursor && { cursor })
});
const response = await api.get(
`/modules/${moduleSlug}/records?${params}`
);
allRecords.push(...response.data.data);
cursor = response.data.meta.next_cursor;
} while (cursor);
return allRecords;
}Choose Reasonable Page Sizes
| Use Case | Recommended Page Size |
|---|---|
| UI display | 25 |
| Background sync | 100 |
| Data export | 100 |
| Search results | 10-25 |
Webhook Efficiency
Process Webhooks Asynchronously
Acknowledge webhooks immediately, then process in the background:
import express from 'express';
import { Queue } from 'bullmq';
const app = express();
const webhookQueue = new Queue('webhooks');
app.post('/webhooks/coherence', async (req, res) => {
// Acknowledge immediately (within 5 seconds)
res.status(200).json({ received: true });
// Queue for async processing
await webhookQueue.add('process-webhook', {
event: req.body.event,
data: req.body.data,
timestamp: req.body.timestamp
});
});Implement Webhook Verification
Verify webhook signatures to ensure authenticity:
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(`sha256=${expected}`)
);
}Error Handling Patterns
Centralized Error Handler
Create a consistent error handling pattern:
class CoherenceAPIError extends Error {
constructor(
public code: string,
message: string,
public status: number,
public details?: Record<string, unknown>
) {
super(message);
this.name = 'CoherenceAPIError';
}
isRetryable(): boolean {
return [429, 500, 502, 503, 504].includes(this.status);
}
}
async function handleAPIResponse(response: Response) {
if (!response.ok) {
const body = await response.json();
throw new CoherenceAPIError(
body.error?.code || 'unknown_error',
body.error?.message || 'An unknown error occurred',
response.status,
body.error?.details
);
}
return response.json();
}Error Response Codes
| Status | Code | Description | Action |
|---|---|---|---|
| 400 | validation_error | Invalid request data | Fix request and retry |
| 401 | authentication_failed | Invalid credentials | Check API key |
| 403 | forbidden | Insufficient permissions | Check scopes |
| 404 | not_found | Resource not found | Verify resource ID |
| 429 | rate_limit_exceeded | Too many requests | Retry with backoff |
| 500 | internal_error | Server error | Retry with backoff |
Idempotency for Safe Retries
Use idempotency keys to safely retry requests without duplicating actions:
curl -X POST "https://api.getcoherence.io/v1/modules/contacts/records" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-12345" \
-d '{"name": "John Smith", "email": "[email protected]"}'Implementation example:
import { v4 as uuidv4 } from 'uuid';
async function createRecordIdempotent(
moduleSlug: string,
data: Record<string, unknown>,
idempotencyKey?: string
) {
const key = idempotencyKey || uuidv4();
return api.post(`/modules/${moduleSlug}/records`, data, {
headers: {
'Idempotency-Key': key
}
});
}Idempotency keys are valid for 24 hours. After that, the same key can be reused.
Monitoring API Usage
Track Usage Metrics
Monitor these key metrics:
- Request count per endpoint
- Error rates by type
- Average response times
- Rate limit utilization
class APIMetrics {
private metrics: Map<string, number[]> = new Map();
recordRequest(endpoint: string, duration: number, status: number): void {
const key = `${endpoint}:${status}`;
if (!this.metrics.has(key)) {
this.metrics.set(key, []);
}
this.metrics.get(key)!.push(duration);
}
getAverageLatency(endpoint: string): number {
const durations = this.metrics.get(`${endpoint}:200`) || [];
if (durations.length === 0) return 0;
return durations.reduce((a, b) => a + b, 0) / durations.length;
}
getErrorRate(endpoint: string): number {
let errors = 0;
let total = 0;
for (const [key, values] of this.metrics) {
if (key.startsWith(endpoint)) {
total += values.length;
if (!key.endsWith(':200')) {
errors += values.length;
}
}
}
return total > 0 ? errors / total : 0;
}
}Performance Optimization
Parallel Requests (Within Limits)
Execute independent requests in parallel while respecting rate limits:
async function fetchMultipleModules(moduleSlugs: string[]) {
const batchSize = 10; // Stay well under rate limit
const results: Record<string, unknown>[] = [];
for (let i = 0; i < moduleSlugs.length; i += batchSize) {
const batch = moduleSlugs.slice(i, i + batchSize);
const batchResults = await Promise.all(
batch.map(slug => api.get(`/modules/${slug}`))
);
results.push(...batchResults.map(r => r.data));
}
return results;
}Connection Pooling
Reuse HTTP connections for better performance:
import { Agent } from 'https';
const agent = new Agent({
keepAlive: true,
maxSockets: 50,
maxFreeSockets: 10,
timeout: 60000
});
const api = axios.create({
baseURL: 'https://api.getcoherence.io/v1',
httpsAgent: agent,
headers: {
'Authorization': `Bearer ${API_KEY}`
}
});Enable Compression
Request compressed responses to reduce bandwidth:
curl "https://api.getcoherence.io/v1/modules/contacts/records" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept-Encoding: gzip, deflate"With axios:
const api = axios.create({
baseURL: 'https://api.getcoherence.io/v1',
headers: {
'Accept-Encoding': 'gzip, deflate'
},
decompress: true
});Enabling gzip compression can reduce response sizes by up to 90% for large JSON payloads.
Related: API Overview | Authentication