Claude-code-plugins-plus-skills klaviyo-rate-limits
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/klaviyo-pack/skills/klaviyo-rate-limits" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-skills-klaviyo-rate-limits && rm -rf "$T"
manifest:
plugins/saas-packs/klaviyo-pack/skills/klaviyo-rate-limits/SKILL.mdsource content
Klaviyo Rate Limits
Overview
Handle Klaviyo's per-account fixed-window rate limits with proper
Retry-After header handling, exponential backoff, and request queuing.
Prerequisites
SDK installedklaviyo-api- Understanding of Klaviyo's dual-window rate limiting
Klaviyo Rate Limit Architecture
Klaviyo uses per-account fixed-window rate limiting with two distinct windows:
| Window | Duration | Limit | Description |
|---|---|---|---|
| Burst | 1 second | 75 requests | Short spike protection |
| Steady | 1 minute | 700 requests | Sustained throughput cap |
Both windows apply simultaneously. Exceeding either triggers a
429 Too Many Requests.
Rate Limit Headers
On successful requests:
| Header | Description |
|---|---|
| Max requests for the window |
| Remaining requests in window |
| Seconds until window resets |
On 429 responses (different headers!):
| Header | Description |
|---|---|
| Integer seconds to wait before retrying |
Critical: When you hit a 429,
headers are NOT returned. OnlyRateLimit-*is present.Retry-After
Instructions
Step 1: Retry-After Aware Backoff
// src/klaviyo/rate-limiter.ts export async function withRateLimitRetry<T>( operation: () => Promise<T>, options = { maxRetries: 5, baseDelayMs: 1000, maxDelayMs: 60000 } ): Promise<T> { for (let attempt = 0; attempt <= options.maxRetries; attempt++) { try { return await operation(); } catch (error: any) { if (attempt === options.maxRetries) throw error; const status = error.status; // Only retry on 429 (rate limit) and 5xx (server errors) if (status !== 429 && (status < 500 || status >= 600)) throw error; let delayMs: number; if (status === 429) { // ALWAYS honor Klaviyo's Retry-After header const retryAfter = error.headers?.['retry-after']; delayMs = retryAfter ? parseInt(retryAfter) * 1000 : options.baseDelayMs * Math.pow(2, attempt); } else { // 5xx: exponential backoff with jitter const exponential = options.baseDelayMs * Math.pow(2, attempt); const jitter = Math.random() * options.baseDelayMs; delayMs = Math.min(exponential + jitter, options.maxDelayMs); } console.log(`[Klaviyo] ${status} on attempt ${attempt + 1}. Retrying in ${delayMs}ms...`); await new Promise(r => setTimeout(r, delayMs)); } } throw new Error('Unreachable'); }
Step 2: Request Queue (Sustained Throughput)
// src/klaviyo/queue.ts import PQueue from 'p-queue'; // Respect Klaviyo's 75 req/s burst limit // Leave headroom: target 60 req/s to avoid hitting the wall const klaviyoQueue = new PQueue({ concurrency: 10, // Max parallel requests interval: 1000, // Per second intervalCap: 60, // 60 requests per second (safe margin) }); export async function queuedKlaviyoCall<T>( operation: () => Promise<T> ): Promise<T> { return klaviyoQueue.add(() => withRateLimitRetry(operation)); } // Monitor queue health klaviyoQueue.on('idle', () => console.log('[Klaviyo] Queue drained')); console.log(`[Klaviyo] Queue: pending=${klaviyoQueue.pending} size=${klaviyoQueue.size}`);
Step 3: Rate Limit Monitor
// src/klaviyo/monitor.ts class RateLimitMonitor { private burstRemaining = 75; private steadyRemaining = 700; private burstResetAt = Date.now(); private steadyResetAt = Date.now(); updateFromHeaders(headers: Record<string, string>): void { const remaining = headers['ratelimit-remaining']; const reset = headers['ratelimit-reset']; if (remaining !== undefined) { this.burstRemaining = parseInt(remaining); } if (reset !== undefined) { this.burstResetAt = Date.now() + parseInt(reset) * 1000; } } shouldThrottle(): boolean { return this.burstRemaining < 10 && Date.now() < this.burstResetAt; } getWaitMs(): number { if (!this.shouldThrottle()) return 0; return Math.max(0, this.burstResetAt - Date.now()); } getStatus(): { burstRemaining: number; shouldThrottle: boolean } { return { burstRemaining: this.burstRemaining, shouldThrottle: this.shouldThrottle(), }; } } export const rateLimitMonitor = new RateLimitMonitor();
Step 4: Bulk Operations with Rate Awareness
// Process large datasets without hitting rate limits export async function bulkProfileSync( profiles: Array<{ email: string; firstName?: string; properties?: Record<string, any> }>, batchSize = 50, // Profiles per batch delayMs = 1000 // Delay between batches ): Promise<{ success: number; failed: number }> { let success = 0; let failed = 0; for (let i = 0; i < profiles.length; i += batchSize) { const batch = profiles.slice(i, i + batchSize); const results = await Promise.allSettled( batch.map(p => queuedKlaviyoCall(() => profilesApi.createOrUpdateProfile({ data: { type: 'profile' as any, attributes: { email: p.email, firstName: p.firstName, properties: p.properties, }, }, }) ) ) ); success += results.filter(r => r.status === 'fulfilled').length; failed += results.filter(r => r.status === 'rejected').length; console.log(`[Klaviyo] Batch ${Math.floor(i / batchSize) + 1}: ${success} ok, ${failed} failed`); // Pace between batches if (i + batchSize < profiles.length) { await new Promise(r => setTimeout(r, delayMs)); } } return { success, failed }; }
Rate Limit Quick Reference
| Endpoint Category | Burst (1s) | Steady (1m) |
|---|---|---|
| Most endpoints | 75 | 700 |
| Create Event | 75 | 700 |
| Bulk Subscribe | 75 | 700 |
| Reporting | Lower (varies) | Lower (varies) |
Error Handling
| Scenario | Detection | Solution |
|---|---|---|
| Burst exceeded | 429 + short Retry-After | Wait Retry-After seconds |
| Steady exceeded | 429 + longer Retry-After | Queue requests, reduce concurrency |
| Thundering herd | Multiple 429s after resume | Add random jitter to retry delays |
| Stuck at 429 | Retry-After keeps growing | Reduce request volume; check for runaway loops |
Resources
Next Steps
For security configuration, see
klaviyo-security-basics.