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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per API key in the current window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix 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 CaseRecommended Page Size
UI display25
Background sync100
Data export100
Search results10-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