Skip to content
OpenSmartRoute
Skillv1.0.0

klaviyo-rate-limits

Implement Klaviyo rate limiting, backoff, and request queuing patterns. Use when handling 429 errors, implementing retry logic, or optimizing API request throughput for Klaviyo. Trigger with phrases l

by jukeyman(0) 0 installs
Free
Sign in to install

Free account. Installing gives you the manifest plus copy-paste snippets.

See reviews

About

Imported from jukeyman/jukeyman-skills (skills/saas-packs--klaviyo-pack--klaviyo-rate-limits/SKILL.md). Install upstream with npx skills add jukeyman/jukeyman-skills --skill saas-packs--klaviyo-pack--klaviyo-rate-limits. Copyright stays with the author (MIT).

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

  • klaviyo-api SDK installed
  • 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
RateLimit-Limit Max requests for the window
RateLimit-Remaining Remaining requests in window
RateLimit-Reset Seconds until window resets

On 429 responses (different headers!):

Header Description
Retry-After Integer seconds to wait before retrying

Critical: When you hit a 429, RateLimit-* headers are NOT returned. Only Retry-After is present.

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.

Use it

Copy one of these into your project. Installing also returns the manifest and these snippets.

yaml
targets:
  - https://api.opensmartroute.ai/api/v1/registry/jukeyman-jukeyman-skills-saas-packs-klaviyo-pack-klaviyo-6f968f/manifest   # or paste the manifest below

Manifest

An Open Capability Manifest: the router reads it to know what this does, what it costs and when to pick it.

jukeyman-jukeyman-skills-saas-packs-klaviyo-pack-klaviyo-6f968f.ocm.jsonjson
{
  "ocm": "1",
  "id": "jukeyman-jukeyman-skills-saas-packs-klaviyo-pack-klaviyo-6f968f",
  "kind": "skill",
  "name": "klaviyo-rate-limits",
  "description": "Implement Klaviyo rate limiting, backoff, and request queuing patterns. Use when handling 429 errors, implementing retry logic, or optimizing API request throughput for Klaviyo. Trigger with phrases like \"klaviyo rate limit\", \"klaviyo throttling\", \"klaviyo 429\", \"klaviyo retry\", \"klaviyo backoff\", \"klaviyo Retry-After\".",
  "publisher": "jukeyman",
  "version": "1.0.0",
  "capabilities": {
    "domains": [
      "coding"
    ],
    "tags": [
      "skill-md",
      "saas",
      "klaviyo",
      "email-marketing",
      "cdp",
      "github"
    ],
    "languages": [
      "en"
    ]
  },
  "quality_prior": 0.6,
  "examples": [
    "Implement Klaviyo rate limiting, backoff, and request queuing patterns. Use when handling 429 errors, implementing retry logic, or optimizing API request throughput for Klaviyo. Trigger with phrases like \"klaviyo rate limit\", \"klaviyo throttling\", \"klaviyo 429\", \"klaviyo retry\", \"klaviyo backoff\", \"klaviyo Retry-After\"."
  ],
  "primary": false,
  "metadata": {
    "source": {
      "provider": "github",
      "repository": "https://github.com/jukeyman/jukeyman-skills",
      "path": "skills/saas-packs--klaviyo-pack--klaviyo-rate-limits/SKILL.md",
      "ref": "99825ad3f3bcc6597ddcc7294a14e6e922f2b67b",
      "url": "https://github.com/jukeyman/jukeyman-skills/blob/99825ad3f3bcc6597ddcc7294a14e6e922f2b67b/skills/saas-packs--klaviyo-pack--klaviyo-rate-limits/SKILL.md",
      "key": "jukeyman/jukeyman-skills/skills/saas-packs--klaviyo-pack--klaviyo-rate-limits/SKILL.md"
    },
    "allowed_tools": [
      "Read,",
      "Write,",
      "Edit"
    ],
    "license": "MIT"
  },
  "instructions": "# Klaviyo Rate Limits\n\n## Overview\n\nHandle Klaviyo's per-account fixed-window rate limits with proper `Retry-After` header handling, exponential backoff, and request queuing.\n\n## Prerequisites\n\n- `klaviyo-api` SDK installed\n- Understanding of Klaviyo's dual-window rate limiting\n\n## Klaviyo Rate Limit Architecture\n\nKlaviyo uses **per-account fixed-window rate limiting** with two distinct windows:\n\n| Window | Duration | Limit | Description |\n|--------|----------|-------|-------------|\n| **Burst** | 1 second | 75 requests | Short spike protection |\n| **Steady** | 1 minute | 700 requests | Sustain",
  "cost": {
    "context_tokens": 1714
  }
}

Fetch it by URL: GET /api/v1/registry/jukeyman-jukeyman-skills-saas-packs-klaviyo-pack-klaviyo-6f968f/manifest?version=1.0.0

Reviews

Star ratings from people who tried it. One review per account; edit yours any time.

No reviews yet. Install it, try it, and be the first to rate it.