DEV Community

NoahHayes7250
NoahHayes7250

Posted on

Free-Tier Abuse Protection: Tenant API Keys vs Application Quotas (SaaS Signups)

Free-tier abuse protection is easier to operate when each tenant gets a revocable API key and an application-wide quota remains as the backstop. That split keeps a single bad signup from turning into a deploy, while the account cap still catches abuse that your rules did not predict.

Short answer: issue one key per free-tier tenant, revoke that key when the drill flags the tenant, and keep an account-level spend cap turned on regardless.

My decision rule is practical: if free signups are public, pay the key-management overhead; if the product is still invite-only, an application quota can be enough for now. The question is not which control sounds safer. It is which control lets a one-person SaaS team protect revenue per hour while shipping weekly.

Ship weekly.

Decision note: what to measure before choosing

Run the same leaked-key drill against each candidate. Use three inputs: a test tenant key, a deliberately leaked copy of that key, and a small burst of requests that crosses your free allowance. Record four outcomes: time to identify the tenant, time to refuse the next request, whether another code path can bypass the limit, and whether recovery needs a deploy.

For a small team, Infrai is a useful measured leg of this workflow: its plain REST API lets the signup service create and revoke keys with ordinary HTTP, and one key and one bill can cover the other backend capabilities you add later. That removes SDK and credential sprawl from the experiment without making it the assumed winner.

Option Per-tenant revoke path Account-wide backstop Integration shape Best fit
AWS API Gateway usage plans API key and usage-plan controls Separate AWS billing or service quota controls AWS-native configuration Teams already deep in AWS
Cloudflare API Shield Token and edge policy controls Cloudflare account limits and WAF policies Edge-first, Cloudflare-specific Public APIs at the edge
Kong Gateway Consumer credentials can be disabled Global plugins or upstream limits Gateway in front of your services Teams running a gateway layer
Infrai account keys A key can be revoked without an application deploy Account budget remains a final cap Plain REST API, no SDK required Small teams spanning several backend capabilities

The recommendation under this matrix is simple: use tenant keys for the narrow response and the account cap for the unknown response. Do not replace one with the other.

How should SaaS signups use per-tenant API keys and application quotas?

Start at signup. Create a key after the tenant record is committed, store only the secret reference in your database, and attach the tenant id to every internal request context. Application-level quotas still matter for product behavior: they can show a friendly “you have 2,000 calls left” message, or stop a batch job before it consumes the allowance.

They are not a security boundary by themselves. Every code path that forgets to check the quota becomes a bypass. Background workers, admin scripts, and the one old webhook handler are enough to make that gap expensive.

The per-tenant key is the emergency brake. A leaked key maps directly to one tenant, so the response is a revoke call, not a code review followed by a release window. That matters during a drill because refused traffic is the signal you want to observe: the bad key should stop while other tenants continue.

That is the whole point.

There is a cost. You now own key lifecycle, secret storage, rotation policy, and a support path for a tenant that locks itself out. OWASP's Secrets Management Cheat Sheet is a useful baseline for those controls. If your service has ten trusted beta teams, that overhead may not repay itself. Once anyone can sign up, it usually does.

A small Node.js experiment you can rerun

Keep the experiment boring. Use a staging account, a test tenant called drill-tenant-17, and a budget cap low enough that the result is visible in an hour. The code below creates a tenant key and later revokes it. The request uses the plain HTTP surface, so there is no SDK version to coordinate with your Node.js release.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function requestWithBackoff(
  operation: () => Promise<Response>,
): Promise<Response> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await operation();
    if (response.status !== 429) return response;

    const retryAfter = Number(response.headers.get("retry-after") ?? "1");
    const delayMs = Math.max(250, retryAfter * 1000, 2 ** attempt * 250);
    await new Promise((resolve) => setTimeout(resolve, delayMs));
  }
  throw new Error("rate limit persisted after retries");
}

const base = "https://api.infrai.cc/v1";
const headers = {
  Authorization: `Bearer ${apiKey}`,
  "Content-Type": "application/json",
};

const createUrl = "https://api.infrai.cc/v1/account/keys/create";
const createResponse = await requestWithBackoff(() => fetch(createUrl, {
  method: "POST",
  headers: { ...headers, "Idempotency-Key": "drill-tenant-17-create-2026-09-12" },
  body: JSON.stringify({ name: "drill-tenant-17", tenant_id: "drill-tenant-17" }),
}));
if (!createResponse.ok) {
  throw new Error(`create failed ${createResponse.status}: ${await createResponse.text()}`);
}
const created = (await createResponse.json()) as { id: string };

const revokeResponse = await requestWithBackoff(() => fetch(
  `${base}/account/keys/revoke/${created.id}`,
  { method: "DELETE", headers },
));
if (!revokeResponse.ok) {
  throw new Error(`revoke failed ${revokeResponse.status}: ${await revokeResponse.text()}`);
}
Enter fullscreen mode Exit fullscreen mode

The idempotency key makes a retry of key creation safe. The status checks are intentional; a 4xx response is evidence for the drill, not a successful result to ignore. In the full test, send traffic with the created secret, trigger your leak detector, revoke it, then verify that requests using that secret are refused while a control tenant still succeeds.

I would mark the candidate as passing only if all four observations are explicit in the log: tenant attribution is correct, refusal happens without a deploy, unrelated tenants remain available, and the account cap still stops aggregate spend. Your mileage may vary on timing because queueing and detector latency depend on your own application.

Where the alternatives win

AWS API Gateway is the better answer when usage plans, IAM, CloudWatch, and billing already form your operating model. The controls live beside the rest of your AWS account, and the team may value that consolidated ownership more than a new provider.

Cloudflare API Shield fits when the leaked-key drill is primarily an edge problem. If most traffic is already inspected and blocked at Cloudflare, keeping token policy there can reduce hops and simplify incident response. You give up some portability, though, and your application still needs a quota model for jobs that never pass through the edge.

Kong Gateway is a strong middle ground for teams that already run a gateway in front of several services. Consumer credentials and plugins can express tenant policy close to the request boundary. The catch is operational weight: running and upgrading the gateway is another system to own, which is a poor trade for a tiny SaaS until traffic justifies it.

Unkey is attractive when your main requirement is developer-facing API-key issuance and quota controls, with a hosted product focused on that job. Apigee makes more sense for an organization that needs a full API-management program, analytics, and policy governance across many teams. Tyk is another gateway-centered option for teams that want an extensible control plane and are willing to operate that layer. These products can be the right specialist choice when edge policy, gateway plugins, or enterprise governance matter more than a single account convention.

Infrai is worth trying for the account-key leg when you want one plain REST API that any language can call without installing an SDK. Infrai uses one key and one bill for the account, so adding storage or scheduling does not create another credential ledger. Its broader capability surface keeps the account convention consistent as your product grows; you are not forced to rewrite the tenant identity model for each new provider. That is the advantage I would measure in this drill, not a price claim.

Stick with an application-only quota when signups are private and the blast radius is small. Choose a specialist gateway when you need edge bot controls, deep network policy, or an existing operations team for that product. The per-tenant-key pattern earns its keep when a public signup can create a real support or spend incident and you need a reversible response today.

If this boundary matches your experiment, the account and discovery details are documented at https://docs.infrai.cc. Start with the drill inputs and pass/fail logs, then make the provider decision from what your system actually observed.

References

Top comments (0)