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:

PlanRequests/minuteRequests/day
Free1001,000
Pro1,00050,000
Enterprise5,000Unlimited

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

StatusCodeDescriptionAction
400validation_errorInvalid request dataFix request and retry
401authentication_failedInvalid credentialsCheck API key
403forbiddenInsufficient permissionsCheck scopes
404not_foundResource not foundVerify resource ID
429rate_limit_exceededToo many requestsRetry with backoff
500internal_errorServer errorRetry 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