Rate Limiting & Best Practices
Understand API rate limits and optimize your integration for performance and reliability
Rate limit headers are live and authoritative. Every /v1 response
includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset
headers, and 429 responses include a Retry-After header. Read these
headers at runtime rather than hard-coding assumptions, and back off on 429
using Retry-After.
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. Limits are enforced per API key over a fixed one-minute window. Requests that exceed the limit receive a 429 Too Many Requests response.
There are no per-plan tiers. Your current limit and remaining allowance are always visible in the response headers on every request — treat those values as the source of truth.
Need higher limits for a high-volume integration? Contact us and we can discuss options.
Rate Limit Headers
Every API response includes headers with your current rate limit status:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 842
X-RateLimit-Reset: 1706745600
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed per API key in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp (seconds) when the current window resets |
The values above are illustrative — always read the live values from your own responses.
Handling 429 Too Many Requests
When you exceed the rate limit, the API returns a Retry-After header (in seconds):
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Response body:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Retry after the window resets.",
"statusCode": 429
}
}Wait at least Retry-After seconds before retrying.
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 acting on many records at once, use the bulk endpoints instead of one call per record. Bulk endpoints accept up to 100 record IDs per request:
// Inefficient: 100 API calls
for (const recordId of recordIds) {
await api.delete(`/modules/contacts/records/${recordId}`);
}
// Efficient: 1 API call
await api.post('/modules/contacts/records/bulk-delete', {
recordIds
});POST /modules/{moduleSlug}/records/bulk similarly applies label and type changes (addLabelIds, removeLabelIds, addTypeIds, removeTypeIds) across up to 100 records in a single request.
Request Only Needed Fields
Use the fields parameter (comma-separated field slugs) to trim each record's fields object to just the data you need:
# Return only the name and email fields on each record (smaller 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
Paging Through Large Datasets
List endpoints use offset pagination via the page and pageSize query parameters (pageSize defaults to 25, maximum 100). List responses return { records, total, page, pageSize }, so you always know how many records remain:
async function fetchAllRecords(moduleSlug: string) {
const allRecords = [];
let page = 1;
const pageSize = 100;
while (true) {
const response = await api.get(
`/modules/${moduleSlug}/records?page=${page}&pageSize=${pageSize}`
);
const { records, total } = response.data;
allRecords.push(...records);
if (records.length === 0 || allRecords.length >= total) break;
page += 1;
}
return allRecords;
}Choose Reasonable Page Sizes
| Use Case | Recommended Page Size |
|---|---|
| UI display | 25 |
| Background sync | 100 |
| Data export | 100 |
| Search results | 10-25 |
The maximum pageSize is 100.
Error Handling Patterns
Centralized Error Handler
Create a consistent error handling pattern around the real error envelope ({ error: { code, message, statusCode } }, plus an issues array on validation errors):
class CoherenceAPIError extends Error {
constructor(
message: string,
public statusCode: number,
public issues?: Array<{ path: (string | number)[]; message: string }>
) {
super(message);
this.name = 'CoherenceAPIError';
}
isRetryable(): boolean {
return [429, 500, 502, 503, 504].includes(this.statusCode);
}
}
async function handleAPIResponse(response: Response) {
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new CoherenceAPIError(
body?.error?.message || 'An unknown error occurred',
body?.error?.statusCode ?? response.status,
body?.error?.issues
);
}
return response.json();
}See Errors for the full envelope reference and status code meanings.
Retrying Mutations Safely
GET requests are safe to retry freely. Be careful retrying mutations (POST, PATCH, DELETE): if a request timed out or failed with a 5xx after the server may have processed it, blindly retrying can create duplicates. Before retrying a create, check whether the record already exists (for example, by listing with a search or filter on a distinguishing field).
Roadmap: idempotent retries for mutations (via an idempotency key) are planned but not yet available.
Monitoring API Usage
Track Usage Metrics
Monitor these key metrics:
- Request count per endpoint
- Error rates by status code
- 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 | Errors