Back to Blog

Fail open or fail closed? What your abuse check should do when it can't answer

Fail open or fail closed? How to decide what your signup and trial abuse check does when it can't get an answer, with safe retries, idempotency keys and per-benefit choices.

Every external check in your sign-up flow raises the same question: what happens if it can't answer? It's a rare case, and it's exactly the kind of decision you don't want to make at 3 a.m. during an incident, with real users waiting and a launch going on. Fail open and your users keep getting the benefit. Fail closed and nothing is granted until the check answers. Neither is right for everything.

This guide shows how to decide, benefit by benefit, and how AbuseZero makes the choice explicit instead of accidental.

The trade-off

Fail openFail closed
While the check can't answerUsers get the benefitNothing is granted
RiskA window where repeat claims get throughA window where real users wait or are turned away
Best forCheap benefits, and ones you can take backExpensive benefits that are spent instantly

The question to ask is simple: what costs more for this benefit, a few unchecked claims or a few real users kept waiting?

YesNoYes, credits or payoutsNoCheck can't answerCan you take the benefitback in full?Fail open, then evaluateagain and take it back ondenyExpensive to deliver?Fail closed, or queue theclaim and retryFail open
YesNoYes, credits or payoutsNoCheck can't answerCan you take the benefitback in full?Fail open, then evaluateagain and take it back ondenyExpensive to deliver?Fail closed, or queue theclaim and retryFail open

Make the choice explicit

The AbuseZero Node.js SDK has an outage option that decides what evaluate() returns when AbuseZero can't be reached, after the SDK's own brief retries:

outageWhat happens
"throw"evaluate() throws a retryable error. Your code decides.
"allow"Returns a local allow, so users keep getting the benefit. Fail open.
"deny"Returns a local deny, so nothing is granted until AbuseZero answers. Fail closed.
lib/abusezero.ts
import { AbuseZero } from "abusezero/server";

export const abusezero = new AbuseZero({
  key: process.env.ABUSEZERO_SECRET_KEY!,
  outage: "allow",
});

A local result carries source: "local_outage", so you can tell it apart from a real evaluation later. Use one client per behavior if different benefits need different choices.

Some cases never become a local decision, whatever you choose. A rate limit always throws, so request volume can never switch protection off. Bot check errors always throw. And a skipped evaluation, for example when a workspace is out of credits, is an answer from AbuseZero, not an outage. See handle outages and retries.

Decided once, not mid-incident

Set the behavior per benefit before launch, and every rare edge case is already handled. Start with Builder or see pricing.

Retry safely with idempotency keys

Every evaluation takes an Idempotency-Key. Store it with the claim and send the same key and the same body on every retry. A retry then returns the original result instead of evaluating twice: no second evaluation and no duplicate claim in your history.

const idempotencyKey = `evaluate:${claim.claimId}`;
await saveClaimAttempt(claim.claimId, idempotencyKey);
const evaluation = await abusezero.evaluate(input, idempotencyKey);

For benefits you evaluate in a background job, use "throw": no user is waiting, so the job simply retries until it gets an answer. See zero-wait free trials.

Our recommendations

BenefitoutageWhy
Free trial you can end"allow"Real users never wait, and you can evaluate again and end the trial on deny
Free AI or API credits"deny", or "throw" and queueCredits are spent instantly; an unchecked window is where farms win
Free plan activation"allow"Low cost per claim, and you can re-check later
Promo codes and referral payouts"deny"Money out the door can't be taken back
Background evaluation jobs"throw"Retry until answered

For the full go-live list, see the indie hacker's launch checklist and the go-live checklist in the docs.

Frequently asked questions

What does fail open mean?

Fail open means that when a check can't answer, the action goes ahead: the user gets the benefit. Fail closed means the action waits or is refused until the check answers.

Should my abuse check fail open or fail closed?

It depends on the benefit. Fail open for cheap benefits and ones you can take back, such as trial days. Fail closed, or queue and retry, for benefits that are spent instantly and cost real money, such as free AI credits.

How do I retry an evaluation without double counting?

Send the same Idempotency-Key and the same request body on every retry. AbuseZero returns the original result instead of evaluating again.

Is a skipped evaluation an outage?

No. A skipped evaluation is an answer from AbuseZero, for example when a workspace is out of credits. Handle it as its own branch.

Build for the rare day

Keep reading