Intercom Performance Tuning
Overview
Optimize Intercom API performance through response caching, efficient search queries, cursor-based pagination, connection pooling, and request batching.
Prerequisites
intercom-clientSDK installed- Understanding of Intercom data model
- Redis or in-memory cache available (optional)
Authentication
All requests authenticate with an Intercom access token passed as a bearer token. Store it as INTERCOM_ACCESS_TOKEN in the environment and let the SDK read it — never hardcode it:
const client = new IntercomClient({ token: process.env.INTERCOM_ACCESS_TOKEN! });
For raw fetch calls, send Authorization: Bearer ${token}.
Intercom API Latency Baselines
| Operation | Typical P50 | Typical P95 | Notes |
|---|---|---|---|
GET /me (health check) |
50ms | 150ms | Lightest endpoint |
GET /contacts/:id |
80ms | 200ms | Single lookup |
POST /contacts/search |
120ms | 400ms | Depends on query complexity |
GET /conversations/:id |
100ms | 300ms | Heavier with parts (up to 500) |
POST /contacts (create) |
150ms | 400ms | Write operation |
GET /contacts (list) |
100ms | 350ms | Paginated, 50 per page |
POST /messages |
200ms | 500ms | Triggers delivery pipeline |
Instructions
Apply these six techniques in order of impact. Each has a complete, copy-pasteable implementation in references/implementation.md; the summaries and the caching skeleton below are enough to follow the workflow at a high level.
- Response caching — wrap contact/conversation reads in an
LRUCache(read-through), and invalidate on update or via webhook so cached data never goes stale. This is the single biggest win for read-heavy integrations. - Efficient search queries — push predicates into the
AND-combinedqueryand request only theper_pageyou need (max 150), rather than fetching broadly and filtering client-side. - Optimized pagination — stream large result sets with an async generator over cursor pagination (
startingAfter) to keep memory flat, and process in fixed-size batches. - Connection pooling — reuse TCP connections with an
https.Agent(keepAlive: true) so you pay the TLS handshake cost once, not per request. - Parallel requests with rate awareness — fan out concurrent lookups through a
p-queuebounded byconcurrency+intervalCapso batches stay under the rate limit. - Performance monitoring — wrap every call in a
measuredCallhelper that emits a structured latency metric, so you can chart real P50/P95 against the baselines above.
The read-through cache skeleton (Step 1) — the foundation everything else builds on:
import { LRUCache } from "lru-cache";
import { IntercomClient } from "intercom-client";
import { Intercom } from "intercom-client";
const contactCache = new LRUCache<string, Intercom.Contact>({
max: 5000,
ttl: 5 * 60 * 1000, // 5 minutes
});
const client = new IntercomClient({ token: process.env.INTERCOM_ACCESS_TOKEN! });
async function getContact(contactId: string): Promise<Intercom.Contact> {
const cached = contactCache.get(contactId);
if (cached) return cached;
const contact = await client.contacts.find({ contactId });
contactCache.set(contactId, contact);
return contact;
}
See references/implementation.md for the full code of all six steps, including invalidation, streaming pagination, connection pooling, the rate-aware queue, and the monitoring wrapper.
Output
Applying these techniques produces:
- A cached read path — repeat contact/conversation lookups served from memory in microseconds instead of an 80–200ms round trip, with correctness preserved via update/webhook invalidation.
- Bounded, streaming iteration — an async generator that walks arbitrarily large contact lists at flat memory, plus a batch processor returning the total count handled.
- Rate-safe concurrency — parallel lookups that stay under Intercom's rate limit, returning a
Map<contactId, Contact>. - Structured latency metrics — one JSON line per call (
{"metric":"intercom.api.call","operation":...,"duration_ms":...,"status":...}) ready to ship to your metrics pipeline and compare against the latency baselines table.
Error Handling
| Issue | Cause | Solution |
|---|---|---|
| Cache stampede | Many concurrent cache misses | Use mutex/lock per key |
| Memory pressure | Cache too large | Set max on LRUCache |
| Stale data | TTL too long | Use webhook invalidation |
| Pagination timeouts | Large data set + slow network | Reduce per_page, add delays |
| Rate limit during batch | Too many parallel requests | Lower PQueue concurrency |
Examples
Quick reference — full runnable versions are in references/examples.md:
- Cached single-contact lookup — read-through cache; first call hits the API, later calls within the TTL are free.
- Narrow search vs broad scan — a BAD 150-row unfiltered page vs a GOOD 25-row targeted query.
- Stream and batch-process every contact — cursor pagination + fixed-size batch flushes over an unbounded list.
- Parallel batch lookup — resolve many IDs concurrently under the rate limit, cache-first.
- Latency instrumentation — wrap any call in
measuredCallto emit a per-call metric line.
Minimal instrumentation example:
const contact = await measuredCall("contacts.find", () =>
client.contacts.find({ contactId: "abc123" })
);
// → {"metric":"intercom.api.call","operation":"contacts.find","duration_ms":84,"status":"success"}
Resources
Next Steps
For cost optimization, see the intercom-cost-tuning skill, which covers request-volume reduction, webhook-driven syncing instead of polling, and tiered caching to lower monthly API spend.