Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement Customer.io load testing and horizontal scaling. Use when preparing for high traffic, running load tests, or designing queue-based architectures for scale. Trigger: "customer.io load test", "customer.io scale", "customer.io high volume", "customer.io k6", "customer.io performance test".
.claude/skills/jeremylongshore-customerio-load-scale/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 13% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 97% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 74% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 58% | 0% |
Load testing and scaling strategies for high-volume Customer.io integrations: k6 load test scripts, scaling architecture selection based on volume tier, Kubernetes HPA autoscaling, message queue buffering, and rate-limit-aware batch processing.
| Daily Events | Architecture | Key Components | |-------------|--------------|----------------| | < 100K | Direct API | Singleton client, retry, connection pooling | | 100K - 1M | Batched API | Event queue, batch processor, rate limiter | | 1M - 10M | Queue-backed | Redis/Kafka queue, worker pool, backpressure | | > 10M | Distributed | Multiple workspaces, sharded queues, regional routing |
Customer.io rate limit is ~100 req/sec per workspace. Plan your architecture around this.
javascript// load-tests/customerio.js // Run: k6 run --vus 10 --duration 60s load-tests/customerio.js import http from "k6/http"; import { check, sleep } from "k6"; import { Counter, Trend } from "k6/metrics"; const SITE_ID = __ENV.CUSTOMERIO_SITE_ID; const API_KEY = __ENV.CUSTOMERIO_TRACK_API_KEY; const BASE_URL = "https://track.customer.io/api/v1"; const AUTH = `${SITE_ID}:${API_KEY}`; const identifyLatency = new Trend("cio_identify_latency"); const trackLatency = new Trend("cio_track_latency"); const errors = new Counter("cio_errors"); export const options = { scenarios: { identify_load: { executor: "ramping-arrival-rate", startRate: 10, timeUnit: "1s", preAllocatedVUs: 20, maxVUs: 50, stages: [ { duration: "30s", target: 50 }, // Ramp to 50/sec { duration: "60s", target: 80 }, // Hold at 80/sec (near limit) { duration: "30s", target: 10 }, // Cool down ], }, }, thresholds: { cio_identify_latency: ["p(95)<500", "p(99)<2000"], cio_track_latency: ["p(95)<500", "p(99)<2000"], cio_errors: ["count<50"], }, }; export default function () { const userId = `k6-load-${__VU}-${__ITER}`; const headers = { "Content-Type": "application/json", Authorization: `Basic ${encoding.b64encode(AUTH)}`, }; // Identify const identifyRes = http.put( `${BASE_URL}/customers/${userId}`, JSON.stringify({ email: `${userId}@loadtest.example.com`, _load_test: true, created_at: Math.floor(Date.now() / 1000), }), { headers } ); identifyLatency.add(identifyRes.timings.duration); check(identifyRes, { "identify 200": (r) => r.status === 200 }) || errors.add(1); // Track event const trackRes = http.post( `${BASE_URL}/customers/${userId}/events`, JSON.stringify({ name: "load_test_event", data: { iteration: __ITER, vu: __VU }, }), { headers } ); trackLatency.add(trackRes.timings.duration); check(trackRes, { "track 200": (r) => r.status === 200 }) || errors.add(1); sleep(0.1); // Small delay between iterations } // Cleanup function — suppress test users after test export function teardown() { console.log("Load test complete. Clean up k6-load-* users in CIO dashboard."); }
Run:
bashk6 run --env CUSTOMERIO_SITE_ID="$CUSTOMERIO_SITE_ID" \ --env CUSTOMERIO_TRACK_API_KEY="$CUSTOMERIO_TRACK_API_KEY" \ load-tests/customerio.js
typescript// services/cio-queue-worker.ts import { Queue, Worker, QueueEvents } from "bullmq"; import { TrackClient, RegionUS } from "customerio-node"; import Bottleneck from "bottleneck"; const REDIS_URL = process.env.REDIS_URL ?? "redis://localhost:6379"; // Rate limiter: 80 requests per second (leave headroom under 100/sec limit) const limiter = new Bottleneck({ maxConcurrent: 15, reservoir: 80, reservoirRefreshAmount: 80, reservoirRefreshInterval: 1000, }); const eventQueue = new Queue("cio:events", { connection: { url: REDIS_URL }, defaultJobOptions: { attempts: 5, backoff: { type: "exponential", delay: 2000 }, removeOnComplete: { count: 10000 }, removeOnFail: { count: 50000 }, }, }); // Producer — your application enqueues events here export async function enqueueEvent( type: "identify" | "track", userId: string, data: Record<string, any> ): Promise<void> { await eventQueue.add(type, { userId, data, enqueuedAt: Date.now() }); } // Consumer — workers process events with rate limiting export function startEventWorkers(concurrency = 10): void { const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); const worker = new Worker( "cio:events", async (job) => { await limiter.schedule(async () => { if (job.name === "identify") { await cio.identify(job.data.userId, job.data.data); } else { await cio.track(job.data.userId, job.data.data); } }); }, { connection: { url: REDIS_URL }, concurrency, } ); worker.on("failed", (job, err) => { console.error(`CIO event failed: ${job?.id} — ${err.message}`); }); // Monitor queue health const events = new QueueEvents("cio:events", { connection: { url: REDIS_URL }, }); setInterval(async () => { const counts = await eventQueue.getJobCounts(); console.log( `CIO queue: waiting=${counts.waiting} active=${counts.active} ` + `failed=${counts.failed} completed=${counts.completed}` ); }, 30000); }
yaml# k8s/hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: cio-worker-hpa spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: cio-event-worker minReplicas: 2 maxReplicas: 20 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Pods pods: metric: name: cio_queue_depth target: type: AverageValue averageValue: "500" behavior: scaleUp: stabilizationWindowSeconds: 60 policies: - type: Pods value: 4 periodSeconds: 60 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Pods value: 2 periodSeconds: 120
typescript// lib/cio-batch-sender.ts import { TrackClient, RegionUS } from "customerio-node"; import Bottleneck from "bottleneck"; export async function batchSend( operations: Array<{ type: "identify" | "track"; userId: string; data: Record<string, any>; }>, ratePerSec = 80 ): Promise<{ succeeded: number; failed: number }> { const cio = new TrackClient( process.env.CUSTOMERIO_SITE_ID!, process.env.CUSTOMERIO_TRACK_API_KEY!, { region: RegionUS } ); const limiter = new Bottleneck({ maxConcurrent: 15, reservoir: ratePerSec, reservoirRefreshAmount: ratePerSec, reservoirRefreshInterval: 1000, }); let succeeded = 0; let failed = 0; const promises = operations.map((op, i) => limiter.schedule(async () => { try { if (op.type === "identify") { await cio.identify(op.userId, op.data); } else { await cio.track(op.userId, op.data); } succeeded++; } catch { failed++; } if ((succeeded + failed) % 1000 === 0) { console.log(`Progress: ${succeeded + failed}/${operations.length}`); } }) ); await Promise.all(promises); return { succeeded, failed }; }
Install: npm install bottleneck bullmq
| Issue | Solution | |-------|----------| | 429 during load test | Reduce rate, check limiter config | | Queue backlog growing | Scale workers, increase concurrency | | Memory pressure | Limit batch and queue sizes, enable GC | | k6 VU exhaustion | Increase preAllocatedVUs and maxVUs |
After load testing, proceed to customerio-known-pitfalls for anti-patterns to avoid.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 20,241 | 10,849 | -46% | 1 | 1 | 0% | 4,508 | 5,097 | +13% | 0 | 0 | — |
case-02 | fail→fail | 16,666 | 15,604 | -6% | 1 | 1 | 0% | 3,584 | 6,203 | +73% | 0 | 0 | — |
case-03 | fail→fail | 16,132 | 12,503 | -22% | 1 | 1 | 0% | 3,033 | 4,986 | +64% | 0 | 0 | — |
case-04 | pass→pass | 13,327 | 12,388 | -7% | 1 | 1 | 0% | 2,788 | 5,446 | +95% | 0 | 0 | — |
case-05 | pass→pass | 14,008 | 13,672 | -2% | 1 | 1 | 0% | 2,857 | 5,527 | +93% | 0 | 0 | — |
case-06 | pass→pass | 14,132 | 10,308 | -27% | 1 | 1 | 0% | 2,342 | 4,596 | +96% | 0 | 0 | — |
case-07 | fail→pass | 18,194 | 14,142 | -22% | 1 | 1 | 0% | 3,800 | 5,756 | +51% | 0 | 0 | — |
case-08 | pass→pass | 4,722 | 2,597 | -45% | 1 | 1 | 0% | 903 | 3,170 | +251% | 0 | 0 | — |
case-09 | fail→fail | 14,304 | 9,325 | -35% | 1 | 1 | 0% | 2,656 | 4,423 | +67% | 0 | 0 | — |
case-10 | pass→pass | 6,553 | 1,975 | -70% | 1 | 1 | 0% | 1,070 | 3,087 | +189% | 0 | 0 | — |
case-11 | fail→pass | 11,175 | 7,671 | -31% | 1 | 1 | 0% | 2,089 | 4,119 | +97% | 0 | 0 | — |
case-12 | fail→fail | 16,226 | 10,244 | -37% | 1 | 1 | 0% | 3,336 | 4,428 | +33% | 0 | 0 | — |
case-13 | fail→fail | 14,490 | 10,447 | -28% | 1 | 1 | 0% | 2,794 | 4,678 | +67% | 0 | 0 | — |
case-14 | fail→pass | 33,049 | 10,419 | -68% | 1 | 1 | 0% | 2,744 | 4,772 | +74% | 0 | 0 | — |
case-15 | pass→pass | 17,876 | 20,637 | +15% | 1 | 1 | 0% | 3,075 | 6,358 | +107% | 0 | 0 | — |
case-16 | pass→fail | 16,941 | 14,886 | -12% | 1 | 1 | 0% | 2,900 | 5,396 | +86% | 0 | 0 | — |
case-17 | fail→pass | 15,576 | 6,979 | -55% | 1 | 1 | 0% | 2,538 | 4,021 | +58% | 0 | 0 | — |
case-18 | fail→fail | 14,383 | 8,645 | -40% | 1 | 1 | 0% | 2,833 | 4,379 | +55% | 0 | 0 | — |
case-19 | fail→pass | 12,333 | 3,706 | -70% | 1 | 1 | 0% | 2,136 | 3,312 | +55% | 0 | 0 | — |
case-20 | fail→fail | 12,843 | 9,235 | -28% | 1 | 1 | 0% | 2,218 | 4,219 | +90% | 0 | 0 | — |
case-21 | pass→pass | 9,691 | 7,715 | -20% | 1 | 1 | 0% | 1,702 | 4,066 | +139% | 0 | 0 | — |
case-22 | pass→pass | 15,231 | 16,844 | +11% | 1 | 1 | 0% | 2,469 | 5,638 | +128% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.