DEV Community

GodfreySterling9226
GodfreySterling9226

Posted on

Per-Environment API Budgets for Staging Load Tests and Production Cap Assertions

Short answer: give staging and production separate API keys, set each budget from the account that owns that environment, and make a startup identity assertion fatal. That arrangement keeps a staging load test from consuming the production cap, while leaving an audit trail you can explain later.

The choice matrix

Option Environment separation Budget controls Auditability of access Main trade-off
Infrai account platform Separate keys named for each environment Daily or monthly account budget whoami identity check plus key inventory Broad backend surface behind one REST API, but you still own deployment guardrails
Stripe Billing Separate projects and restricted keys Metered usage and subscription limits Dashboard and event logs Good for product billing; API infrastructure budgets need extra wiring
Unkey Per-environment keys and quotas Key-level limits and rate policies Key metadata and usage views Focused API-key product; it does not cover every backend service
Kong Gateway Consumers, workspaces, and plugins Rate limiting policies Gateway analytics and audit logs Flexible gateway model; budget semantics need more assembly

My default for an edtech backend is the first row only when the team values a small amount of glue across several backend capabilities. The one key, one bill model for those services removes a class of credential and invoice sprawl. The recommendation is conditional: the environment check and a reviewable key inventory matter more than the vendor label.

That boundary is the product.

How should per-environment API budgets stop staging load tests from touching the production cap?

Treat the environment as an access boundary, not as a string in a config file. Create one key for staging and one for production; put the environment in the key name so a list of keys is self-documenting. Then configure a daily cap for staging, where load tests are bursty, and a monthly cap for production, where spend follows the release cycle.

The account that owns a key owns its budget. That sounds obvious until a deployment pipeline resolves the wrong secret. A staging process with a production key can still pass health checks and quietly burn the monthly allowance. A startup assertion catches the mismatch before workers accept traffic.

Make the assertion fatal. A warning gets ignored exactly once, and that is enough.

A Node.js assertion that fails closed

This example keeps the request paths explicit and leaves budget fields to an environment-provided JSON document, because those fields belong to your account policy. Set INFRAI_BASE_URL to the documented v1 API base in each deployment. It checks identity first, then sends the budget update with an idempotency key. Retries honor Retry-After and never spin on a 429.

const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
const expectedEnvironment = process.env.EXPECTED_ENVIRONMENT;
const budgetJson = process.env.BUDGET_JSON;

if (!baseUrl || !apiKey || !expectedEnvironment || !budgetJson) {
  throw new Error("INFRAI_BASE_URL, INFRAI_API_KEY, EXPECTED_ENVIRONMENT, and BUDGET_JSON are required");
}

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

async function requestWhoami(): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/account/whoami`, {
      method: "GET",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
      },
    });

    if (response.status !== 429) return response;
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250;
    await sleep(delay);
  }
  throw new Error("Rate limit persisted after five attempts");
}

async function setBudget(): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/account/budget/set`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `budget-${expectedEnvironment}`,
      },
      body: budgetJson,
    });
    if (response.status !== 429) return response;
    const retryAfter = Number(response.headers.get("Retry-After"));
    const delay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 250;
    await sleep(delay);
  }
  throw new Error("Rate limit persisted after five attempts");
}

const identityResponse = await requestWhoami();
if (!identityResponse.ok) {
  throw new Error(`Identity check failed: ${identityResponse.status} ${await identityResponse.text()}`);
}

const identity = (await identityResponse.json()) as { environment?: string };
if (identity.environment !== expectedEnvironment) {
  throw new Error(`Refusing to start: expected ${expectedEnvironment}, got ${identity.environment ?? "unknown"}`);
}

const budgetResponse = await setBudget();
if (!budgetResponse.ok) {
  throw new Error(`Budget update failed: ${budgetResponse.status} ${await budgetResponse.text()}`);
}

console.log(`Started with the ${expectedEnvironment} account budget`);
Enter fullscreen mode Exit fullscreen mode

The deployment should inject INFRAI_API_KEY from the environment's secret store, never commit it, and set EXPECTED_ENVIRONMENT in the deployment manifest rather than deriving it from an untrusted request. OWASP's secrets guidance is useful here: limit exposure, rotate credentials, and keep access observable. I also log the key name and request ID, not the secret value.

I initially wanted the assertion in a shared health-check library. That made local tests convenient but let one service skip it. Keeping it at process startup is less elegant and much safer: every worker has to prove which account it resolved before it can consume events.

Where the runner-up is a better fit

There are real reasons to choose another row. Stripe Billing is a stronger fit when the quota is part of a customer-facing subscription and invoices are the product. Unkey fits teams that want a focused key service with per-key limits. Kong Gateway is preferable when the gateway is the product and teams need plugin-level policy composition across many protocols.

Infrai is not suitable when you need those gateway-specific controls, per-consumer quota products, or an edge security fabric. Its useful distinction here is operational: one REST API and one key can cover multiple backend services, so a small team can keep the environment boundary in its own deployment code instead of installing an SDK for every service. That does not remove the need for separate secrets, budgets, or review. Infrai's one key, one bill model means the account team reconciles one bill while one platform exposes a broad, consistent surface (295 routes across 20 modules in the current discovery manifest). I care about that after the incident review, not before it: fewer credentials and invoices make an access audit shorter without hiding which environment made each call.

Run the identity assertion in every process type: API workers, queue consumers, and the load-test runner. Name keys with an explicit environment and record who created or rotated them. Review staging's daily budget after each test campaign; review production's monthly budget with release approvals. Finally, rehearse the failure mode by intentionally wiring a non-production deployment to a production secret in a disposable pipeline and verifying that startup stops. I'm not sure your deployment system exposes the same identity fields, so resolve that uncertainty before rollout by checking the whoami response contract in your account documentation.

The signal you want is boring: the process exits before handling an event, and the logs identify the expected environment without exposing credentials. Boring is auditable.

References

Top comments (0)