Documentation

Load Testing a Metered API

Plan load tests from the per-minute rate limit, pace virtual users against it, and account for errors and retries that consume the same budget.


Load testing a metered API is a budget exercise before it is a traffic exercise. The monthly quota says how much you may spend, but the per-minute limit says how fast you may spend it, and that is the number your test has to respect. A test key is always capped at 60 requests per minute, on every plan, and Free organizations only have test keys. A run with hundreds of virtual users against a test key therefore measures your own retry behavior, not the service. To exercise a ceiling above 60, use a live key on that plan in an isolated environment.

Concurrency follows from rate and latency. For a closed loop, workers needed is roughly requests per second * average latency in seconds. At 120 requests per minute and 250 ms per response, that is 0.5 workers, so one paced worker can sustain the rate. Extra workers do not raise a limit; they create bursts that end in 429.

PlanRequests/minRequests/secWorkers at 250 ms
Starter12021
Growth600103
Growth Plus3,0005013
Business6,00010025

A paced k6 scenario

ramping-arrival-rate schedules request starts rather than looping as fast as each virtual user can. The key comes from the environment and is shared by every virtual user, so the whole run draws on one key’s minute bucket.

import http from "k6/http";
import { check } from "k6";
import { Rate } from "k6/metrics";

const ok200 = new Rate("ok_200");
const throttled = new Rate("throttled_429");
const KEY = __ENV.CURRENCYCORE_KEY; // live key on the plan under test

export const options = {
  scenarios: {
    rates: {
      executor: "ramping-arrival-rate",
      startRate: 10,
      timeUnit: "1m",
      preAllocatedVUs: 20,
      maxVUs: 80,
      stages: [
        { target: 60, duration: "2m" },
        { target: 120, duration: "3m" },
        { target: 120, duration: "5m" },
        { target: 0, duration: "1m" },
      ],
    },
  },
  thresholds: {
    ok_200: ["rate>0.99"],          // self-imposed, not an API promise
    http_req_duration: ["p(95)<800"],
    checks: ["rate>0.99"],
  },
};

export default function () {
  const res = http.get("https://api.currency-core.com/v1/rates/EUR", {
    headers: { Authorization: `Bearer ${KEY}` },
  });
  ok200.add(res.status === 200);
  throttled.add(res.status === 429);
  check(res, { "completed": (r) => r.status === 200 || r.status === 429 });
}

The per-minute limit counts every request, including errors and retries. A failed call is still a withdrawal from the same minute bucket, so a test that retries aggressively can exhaust the budget without producing a single success. Keep thresholds tied to your own service objectives.

There are no published load-test figures or latency commitments for the API, so treat any number above as a test harness setting. Watch throttled_429; if it rises during the plateau, your arrival rate is above the bucket, not below the service’s capacity.