Outage policy
What the browser does when it cannot reach us, what your server should do when it cannot, and why blocking our domain is not a bypass.
Two sides, two policies
An outage can be seen from the browser, which cannot reach our API, or from your server, which cannot reach the verify endpoint. The loader's behaviour on its side is fixed and described first. Your server's behaviour is yours to choose, and the second half of this page is our recommendation.
The browser cannot reach us
When the challenge request fails, with a timeout, a connection error or a 5xx, the widget still fires ready, and showBox() still produces a pass: an offline pass with the same five strings, a lot_number that starts with offline and a captcha_output of offline. No box is shown and success fires. The same happens if the service disappears between the challenge and the verification. Your page code therefore never has to handle "the captcha is down": the form is submitted as usual.
Each call has a 2.5 second timeout and one retry, so this takes at most about 5.3 seconds. A 4xx answer is not an outage: a refused site key fires error instead, and no offline pass is made.
The offline pass is worthless on its own. The verify endpoint refuses it with code -50306 whenever it is reachable. So a visitor, or a bot, who blocks our domain in their own browser does not skip the check: their form reaches your server with an offline pass, your server asks us, and we say no. The only case in which an offline pass gets through is the one where your server cannot reach us either, and that is the case your own policy decides.
Your server cannot reach us
Every HTTP 200 from the verify endpoint is a decision, and a decision is always honoured. Anything else, a timeout, a connection error or a 5xx, means there was no decision, and your server has to choose. There are two sane choices, and they should both exist in your configuration:
| Policy | On no decision | Protects against | Costs |
|---|---|---|---|
| Fail open | Let the visitor through, log it, count it. | Your sign-ups never depend on us being up. | An attacker who can take our service down for everyone gets through while it is down. |
| Fail closed | Refuse the form, log it, count it. | Nothing gets through without a decision. | Our outage becomes your outage on every protected form. |
We recommend fail open as the default, with fail closed available per site as a switch you can flip during an attack that is exploiting the open policy, and flip back afterwards. Keep the switch in configuration, not in code, so either direction takes seconds and needs no deploy.
// On your server. FAIL_OPEN is configuration, not code, so it can be flipped in seconds.
async function gate(pass) {
let answer;
try {
answer = await confirmPass(pass); // throws on timeout, connection error or non-200
} catch (e) {
metrics.increment('captcha_no_decision'); // alert when this rises
log.warn('captcha: no decision', e.message);
return config.FAIL_OPEN ? ALLOW : REJECT;
}
return answer.ok ? ALLOW : REJECT; // a decision is always honoured
}Watch the counter
Fail open is only safe if somebody notices. Count every "no decision" and alert when the count rises over a few minutes: a healthy integration sees none. If you also count refusals by code, a rise in -50306 with offline lot numbers tells you that browsers cannot reach us while your server can, which is usually a content security policy, an ad blocker or a corporate firewall on the visitors' side rather than an outage. See Troubleshooting.
Use a timeout of about five seconds on the call. A retry after a network error is safe: an identical call within 20 seconds gets the same answer.
What a 4xx means
A 4xx from the verify endpoint is also not a decision from us. 405 means the method was not POST, and 403 means the endpoint has an address allow-list that does not include your server; both are integration faults to fix, not outages to fail over. We suggest counting them separately from "no decision" so they are not hidden by a fail-open policy, and treating them as a refusal until fixed.