FR EN

Handling outages

What should happen when your code calls check() and Agreely cannot be reached? Agreely's answer is deny, by default, everywhere. This guide explains why, and how to make a narrow, audited exception when you genuinely need one.

Fail-closed is the default

When Agreely is unreachable (a 503, a timeout, or a network error):

  • check() returns false (deny);
  • checkDetailed() throws AgreelyUnavailableError;
  • the CLI exits 4 (distinct from a deny's 10).

A real 200 deny is never affected by any of this. An outage is not a denial, but the safe default for an outage is to behave as if denied, so an infrastructure problem can never silently grant access to data.

Distinguish outage from deny

Use checkDetailed() (or the CLI exit code) when you need to tell the two apart. A deny is an authoritative answer you must honor; an outage is a transient failure you may retry, alert on, or, for a chosen category, degrade. check() collapses both to false on purpose, so the simplest integration is always safe.

The fail-open exception

Some categories are low-risk enough that denying them during an outage is worse than allowing them (for example, a non-sensitive analytics signal). The SDKs let you fail open for such categories, but only through independent gates, so it can never happen by accident and is always audited.

TypeScript: three gates

const agreely = new Agreely({
  apiKey,
  degradeOnOutage: {
    mode: "fail-open",                 // gate 1a: the explicit word
    categories: ["Browsing/usage"],    // gate 1b: ONLY these may ever degrade
    maxOutageWindow: "5m",             // refuse to degrade past this
    onDegrade: (ctx) => audit.log(ctx) // MANDATORY, without it, the constructor throws
  },
});

// gate 2: the call must ALSO opt in. Effective only because the category is
// allow-listed. Without the config, this per-call opt-in still denies.
await agreely.check("cust_8812", "Browsing/usage", "Analytics", { onOutage: "allow" });

// gate 3 (operator): break-glass, for an active incident.
agreely.breakGlass.engage({ reason: "incident-4471", ttl: "30m", scope: ["Browsing/usage"] });

PHP: two gates

PHP ships the same config allow-list + per-call opt-in. It omits break-glass in v1, because a request-scoped runtime cannot hold a fleet-wide "engaged" state honestly (see the PHP SDK).

$agreely = new Agreely([
    'apiKey' => $key,
    'degradeOnOutage' => [
        'mode'            => 'fail-open',
        'categories'      => ['Browsing/usage'],
        'maxOutageWindow' => '5m',
        'onDegrade'       => fn ($ctx) => $audit->log($ctx), // MANDATORY
    ],
]);
$agreely->check('cust_8812', 'Browsing/usage', 'Analytics', ['onOutage' => 'allow']);

The guardrails

What fail-open will not let you do

  • No silent degrade. onDegrade is mandatory; without an audit sink the constructor throws. Every degraded allow emits an evidence record, and every break-glass-authorized allow emits its own event.
  • No unbounded window. Both ttl and maxOutageWindow are capped at 24h; over the cap throws AgreelyConfigError.
  • No allow-cache. There is no cache of allow decisions; fail-open is computed per call during an outage, never replayed from a stored allow.
  • No accidental opt-in. A per-call onOutage: "allow" with no matching allow-listed category has no effect (the check still denies) and logs a one-time warning.

Tuning 429s, not outages

A rate limit (429) is not an outage: it is a signal to slow down. Two constructor options tune it, without ever touching the fail-closed policy. maxRetries (default 0) adds extra attempts on a 429 only and only for idempotent calls (GET and check); a mutating call never retries. respectRetryAfter (default true) honors the server's Retry-After header. An outage, by contrast, is never silently retried: it denies (or throws AgreelyUnavailableError) and leaves the decision to you.

Recommendation

Keep the default. Fail-closed is correct for anything that touches sensitive data or carries a compliance obligation, which is most of what you will gate. Reach for fail-open only for a specific, low-risk category where availability genuinely outweighs strict enforcement, and wire its onDegrade sink into the same audit trail you would show a regulator.

Next