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 open | Fail closed | |
|---|---|---|
| While the check can't answer | Users get the benefit | Nothing is granted |
| Risk | A window where repeat claims get through | A window where real users wait or are turned away |
| Best for | Cheap benefits, and ones you can take back | Expensive 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?
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:
outage | What 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. |
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
| Benefit | outage | Why |
|---|---|---|
| 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 queue | Credits 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
Start with Builder
Pay as you go. Outage behavior built in.
Handle outages and retries
The full guide in the docs.
See pricing
Builder, Pro, Scale and Enterprise.