Skillshub apollo-rate-limits
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/apollo-rate-limits" ~/.claude/skills/comeonoliver-skillshub-apollo-rate-limits && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/apollo-rate-limits/SKILL.mdsource content
Apollo Rate Limits
Overview
Implement robust rate limiting and backoff for the Apollo.io API. Apollo uses fixed-window rate limiting with per-endpoint limits. Unlike hourly quotas, Apollo limits are per minute with a burst limit per second. Exceeding them returns HTTP 429.
Prerequisites
- Valid Apollo API key
- Node.js 18+
Instructions
Step 1: Understand Apollo's Rate Limit Structure
Apollo's official rate limits (as of 2025):
Endpoint Category | Limit/min | Burst/sec | Notes ----------------------------+-----------+-----------+------------------------------- People Search | 100 | 10 | /mixed_people/api_search (free) People Enrichment | 100 | 10 | /people/match (1 credit each) Bulk People Enrichment | 10 | 2 | /people/bulk_match (up to 10/call) Organization Search | 100 | 10 | /mixed_companies/search Organization Enrichment | 100 | 10 | /organizations/enrich Contacts CRUD | 100 | 10 | /contacts/* Sequences | 100 | 10 | /emailer_campaigns/* Deals | 100 | 10 | /opportunities/*
Response headers on every successful call:
— max requests per windowx-rate-limit-limit
— requests remaining in current windowx-rate-limit-remaining
— seconds to wait (only on 429 responses)retry-after
Step 2: Build a Per-Endpoint Rate Limiter
// src/apollo/rate-limiter.ts export class SlidingWindowLimiter { private timestamps: number[] = []; constructor( private maxRequests: number = 100, private windowMs: number = 60_000, ) {} async acquire(): Promise<void> { const now = Date.now(); // Remove timestamps outside the window this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs); if (this.timestamps.length >= this.maxRequests) { const oldestInWindow = this.timestamps[0]; const waitMs = this.windowMs - (now - oldestInWindow) + 100; console.warn(`[RateLimit] At capacity (${this.maxRequests}/${this.windowMs}ms). Waiting ${waitMs}ms`); await new Promise((r) => setTimeout(r, waitMs)); } this.timestamps.push(Date.now()); } get remaining(): number { const now = Date.now(); this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs); return this.maxRequests - this.timestamps.length; } } // Create limiters per endpoint category export const limiters = { search: new SlidingWindowLimiter(100, 60_000), enrichment: new SlidingWindowLimiter(100, 60_000), bulkEnrichment: new SlidingWindowLimiter(10, 60_000), contacts: new SlidingWindowLimiter(100, 60_000), sequences: new SlidingWindowLimiter(100, 60_000), };
Step 3: Exponential Backoff with Retry-After
// src/apollo/backoff.ts export async function withBackoff<T>( fn: () => Promise<T>, opts: { maxRetries?: number; baseMs?: number; maxMs?: number } = {}, ): Promise<T> { const { maxRetries = 5, baseMs = 1000, maxMs = 60_000 } = opts; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (err: any) { const status = err.response?.status; if (status !== 429 && status < 500) throw err; if (attempt === maxRetries) throw err; // Prefer Retry-After header, fall back to exponential backoff const retryAfter = err.response?.headers?.['retry-after']; const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(baseMs * 2 ** attempt + Math.random() * 500, maxMs); console.warn(`[Apollo] ${status} attempt ${attempt + 1}/${maxRetries + 1}, retry in ${Math.round(delayMs / 1000)}s`); await new Promise((r) => setTimeout(r, delayMs)); } } throw new Error('Unreachable'); }
Step 4: Request Queue with Concurrency Control
// src/apollo/queue.ts import PQueue from 'p-queue'; import { limiters } from './rate-limiter'; type EndpointCategory = keyof typeof limiters; const queues: Record<EndpointCategory, PQueue> = { search: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), enrichment: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), bulkEnrichment: new PQueue({ concurrency: 2, intervalCap: 2, interval: 1000 }), contacts: new PQueue({ concurrency: 5, intervalCap: 10, interval: 1000 }), sequences: new PQueue({ concurrency: 3, intervalCap: 5, interval: 1000 }), }; export async function queuedRequest<T>( category: EndpointCategory, fn: () => Promise<T>, ): Promise<T> { await limiters[category].acquire(); return queues[category].add(() => fn()) as Promise<T>; }
Step 5: Monitor Rate Limit Usage via Response Headers
// src/apollo/rate-monitor.ts import { AxiosInstance, AxiosResponse } from 'axios'; export function attachRateMonitor(client: AxiosInstance) { client.interceptors.response.use((response: AxiosResponse) => { const limit = response.headers['x-rate-limit-limit']; const remaining = response.headers['x-rate-limit-remaining']; if (limit && remaining) { const pct = Math.round(((parseInt(limit) - parseInt(remaining)) / parseInt(limit)) * 100); if (pct >= 80) { console.warn(`[Apollo] Rate limit ${pct}% used (${remaining}/${limit} remaining) on ${response.config.url}`); } } return response; }); }
Output
- Per-endpoint sliding window rate limiter matching Apollo's actual limits
- Exponential backoff respecting
headersretry-after
-based request queue with per-second burst controlPQueue- Response header monitoring with 80% warning threshold
Error Handling
| Scenario | Strategy |
|---|---|
429 with | Wait the specified seconds, then retry |
| 429 without header | Exponential backoff: 1s, 2s, 4s, 8s, up to 60s |
| Bulk enrichment limited | Use dedicated queue with 2/sec burst limit |
| Near quota (>80%) | Log warning, defer non-critical requests |
Examples
Bulk Search with Rate Limiting
import { queuedRequest } from './apollo/queue'; import { withBackoff } from './apollo/backoff'; const domains = ['stripe.com', 'notion.so', 'linear.app', /* ... */]; const results = await Promise.all( domains.map((domain) => queuedRequest('search', () => withBackoff(() => client.post('/mixed_people/api_search', { q_organization_domains_list: [domain], per_page: 25, }), ), ), ), ); console.log(`Searched ${results.length} domains within rate limits`);
Resources
Next Steps
Proceed to
apollo-security-basics for API security best practices.