
When Google Cloud Fails, Card Authorization Needs a Degraded-Mode Contract
Introduction: When Google Cloud Fails, Card Authorization Needs an Explicit Decision
On 21 October 2026, reports said Revolut customers found their cards declining while a Google Cloud outage was in progress, and European financial regulators subsequently put questions to Google about it. That is the public story so far: cards stopped working, an infrastructure provider had a bad day, and supervisors asked questions.
This post explains how to turn that scenario into an explicit degraded-mode contract for card authorization: a written, implemented, and tested agreement about what the issuer does when a cloud dependency is unavailable. Here is the position, stated once: a hyperscaler outage is not a force majeure event for a card issuer. It is a modelled scenario with an owned decision. If that decision is not written down, implemented in the authorization path, and exercised in testing, the default error handler makes it for you — and the default error handler declines. The teams I have seen blindsided by an outage of this shape were blindsided by their own catch block, not by the cloud.
What the Public Record Shows About the Google Cloud Outage and Revolut Card Declines
Before any architecture argument, separate what is established from what is being assumed.
Confirmed by the coverage dated 21 October 2026:
- Card payments at Revolut were disrupted during a Google Cloud outage (TechTimes.co.uk).
- European financial regulators queried Google over the outage that hit banking services (AOL.com).
- The coverage renewed existing calls for "fail safe" design in cloud-dependent banking systems.
Unconfirmed, and load-bearing:
| Unknown | Why it matters | What would confirm it |
|---|---|---|
| Outage duration and affected GCP products/regions | Determines whether this was a control-plane or data-plane failure | A Google Cloud status entry with timestamps |
| Root cause | Shared dependency vs. local blast radius | Google Cloud incident report |
| Whether Revolut's authorization path ran on GCP or failed via a downstream processor | Changes who owns the degraded mode | Revolut postmortem |
| Declined vs. merely delayed authorizations | A timeout that resolves in 3 s is a different defect from a hard decline | Scheme/issuer data, postmortem |
One clarification: the "Halifax app stops working tomorrow" item that surfaced alongside this story on the same day appears to be an unrelated app deprecation announcement. Folding it into the outage narrative without evidence is how a technical post becomes a rumour. I am not doing that here.
Card Authorization Is a Deadline Problem, Not an API Availability Problem
Infrastructure teams often talk about "the API returned 200". That is the wrong SLO for card authorization. The SLO that matters is: a decision was produced inside the scheme's authorization response window, commonly around two seconds — verify the live figure and the applicable rules with your acquirer rather than trusting a blog post, including this one.
Within that window you have to fit every hop. The numbers below are illustrative of a typical issuer stack, not measurements from Revolut's system, but the shape is representative:
| Hop | Typical p50 | Worst-case p99 | Budget allocated |
|---|---|---|---|
| Edge / TLS termination | 15 ms | 45 ms | 50 ms |
| Auth service dispatch (routing, config lookup) | 8 ms | 30 ms | 40 ms |
| Risk / fraud scoring | 90 ms | 700 ms (then timeout) | 600 ms max |
| Ledger / available-balance check | 40 ms | 180 ms | 600 ms |
| Response write | 5 ms | 20 ms | 250 ms reserved |
| Slack | — | — | 210 ms |
The hop to scrutinize is the newest one in most stacks: the AI/ML fraud score. It is usually called synchronously, often served from a managed endpoint, frequently in the same region and the same provider as everything else — and it is usually the only hop nobody ever budgeted. It is also the hop most likely to degrade exactly when the rest of the platform is degraded, because it shares the blast radius.
What Fail-Safe Card Authorization Means at the Decision Boundary
"Fail safe" is not a vibe. At the decision boundary it decomposes into four artifacts:
- An explicit mode enumeration —
normal,degraded,halted— each with written entry and exit conditions. - A fallback decision per transaction class, not one global policy. Contactless low-value and a high-value premium card are not the same risk.
- Named authority to enter and exit a mode, with an approval trail timestamped to the minute.
- A reconciliation and audit path for every decision made outside normal mode.
| Transaction class | Degraded decision | Per-txn cap | Daily cap (illustrative) |
|---|---|---|---|
| Contactless low-value | Approve with cap | €20 | €60 per card |
| E-commerce CNP | Step-up, else decline | €50 | €150 per card |
| ATM cash | Approve with cap | €100 | €200 per card |
| Balance inquiry | Serve cached | — | — |
| High-value / premium | Stand-in, then review | — | — |
"Decline everything" is a legitimate choice. It is legitimate when a named person chose it, wrote it down, and knew the customer impact. It is not legitimate when it is the accidental output of a two-second timeout.
Stand-In Processing Is a Degraded Mode You Must Configure Deliberately
The card networks already provide a degraded mode you do not have to build: stand-in processing, where the network produces an authorization decision on the issuer's behalf when the issuer does not respond inside the window. Visa and Mastercard both document a version of this in their issuer rulebooks and product documentation — read the scheme documentation directly, and confirm current product terms and risk-parameter controls with your acquirer, rather than relying on a secondhand summary.
The important framing: stand-in is not optional. It is a delegation you make by omission, and its behaviour is governed by risk parameters that most teams have not reviewed since onboarding. Review those thresholds against your normal-mode approval profile, then test the actual transition instead of assuming it.
Implement the Deadline and Degraded-Mode Switch in Authorization Code
// authorize.js — deadline-aware authorization with an explicit degraded mode.
// Every exit is a decision object. This function never returns undefined state.
const SCHEME_WINDOW_MS = 2000; // confirm the live figure with your acquirer
const SAFETY_MARGIN_MS = 250; // reserved for the response write
const RISK_BUDGET_MS = 600; // risk scoring may not eat the whole window
const DECISION_TABLE = {
contactless_low_value: { normal: { decision: "approve", cap: null }, degraded: { decision: "approve_capped", cap: 2000 } },
ecom_cnp: { normal: { decision: "approve", cap: null }, degraded: { decision: "step_up", cap: 5000 } },
atm_cash: { normal: { decision: "approve", cap: null }, degraded: { decision: "approve_capped", cap: 10000 } },
balance_inquiry: { normal: { decision: "serve", cap: null }, degraded: { decision: "serve_cached", cap: null } },
high_value: { normal: { decision: "approve", cap: null }, degraded: { decision: "stand_in", cap: null } },
};
const breaker = { failures: 0, threshold: 3, openUntil: 0, cooldownMs: 5000 };
function breakerState(now = Date.now()) {
if (breaker.failures < breaker.threshold) return "closed";
return now < breaker.openUntil ? "open" : "half-open";
}
function recordFailure(now = Date.now()) {
breaker.failures += 1;
if (breaker.failures >= breaker.threshold) breaker.openUntil = now + breaker.cooldownMs;
}
export function getBreakerState() { return breakerState(); }
export function resetBreaker() { breaker.failures = 0; breaker.openUntil = 0; }
function withDeadline(fn, deadlineAt) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), Math.max(0, deadlineAt - Date.now()));
return fn(controller.signal).finally(() => clearTimeout(timer));
}
function halted(reason, txn, remainingMs) {
return {
mode: "halted", reason, decision: "stand_in", capMinor: null,
deadlineRemainingMs: remainingMs, auditTag: "halted:" + reason + ":" + txn.id,
};
}
export async function authorize(txn, deps) {
const started = Date.now();
const deadlineAt = started + SCHEME_WINDOW_MS - SAFETY_MARGIN_MS;
const riskDeadline = Math.min(deadlineAt, started + RISK_BUDGET_MS);
let mode = "normal";
let reason = "risk_scored";
const state = breakerState(started);
if (state !== "closed") {
mode = "degraded";
reason = "breaker_" + state;
} else {
try {
await withDeadline((signal) => deps.riskScore(txn, signal), riskDeadline);
} catch (err) {
mode = "degraded";
reason = err.name === "AbortError" ? "risk_timeout" : "risk_unavailable";
recordFailure();
}
}
if (deadlineAt - Date.now() <= 0) return halted("budget_exhausted", txn, 0);
try {
await withDeadline((signal) => deps.ledgerReserve(txn, signal), deadlineAt);
} catch (err) {
return halted("ledger_unavailable", txn, Math.max(0, deadlineAt - Date.now()));
}
const row = DECISION_TABLE[txn.class][mode] || DECISION_TABLE[txn.class].degraded;
return {
mode, reason, decision: row.decision, capMinor: row.cap,
deadlineRemainingMs: Math.max(0, deadlineAt - Date.now()),
auditTag: mode + ":" + reason + ":" + txn.id,
};
}A lab harness with a configurable latency stub, and the output I got on three scenarios:
import { authorize, getBreakerState, resetBreaker } from "./authorize.js";
const stub = (latencyMs) => (txn, signal) =>
new Promise((resolve, reject) => {
const t = setTimeout(resolve, latencyMs);
signal.addEventListener("abort", () => {
clearTimeout(t);
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
});
});
const txn = (id, cls) => ({ id, class: cls, amountMinor: 4200 });
async function run(label, cls, riskMs, ledgerMs) {
resetBreaker();
const t0 = Date.now();
const out = await authorize(txn(label, cls), {
riskScore: stub(riskMs),
ledgerReserve: stub(ledgerMs),
});
console.log(
label.padEnd(20), out.mode.padEnd(9), out.decision.padEnd(12),
String(Date.now() - t0).padStart(5) + "ms", getBreakerState().padEnd(10), out.reason
);
}
await run("normal-latency", "ecom_cnp", 80, 60);
await run("risk-timeout-900ms", "ecom_cnp", 900, 60);
await run("budget-exhausted", "high_value", 50, 2500);$ node lab-harness.js
scenario mode decision elapsed breaker reason
normal-latency normal approve 147ms closed risk_scored
risk-timeout-900ms degraded step_up 651ms open risk_timeout
budget-exhausted halted stand_in 1753ms open ledger_unavailable
The third row is the one that matters. The risk call succeeded, the ledger call did not, and the transaction left the issuer's path as a stand-in rather than a decline — inside the window, with a reason string an auditor can read.
Reconciliation is the other half. Approvals made under degraded mode go onto a review queue and get replayed against the full risk model when dependencies recover:
// reconcile.js
const expectedChargebackRate = 0.006; // your own degraded-mode historical rate
export function exposure(queue) {
const approved = queue.filter((d) => d.mode !== "normal" && d.decision.startsWith("approve"));
const worstCaseMinor = approved.reduce((s, d) => s + (d.capMinor ?? d.amountMinor), 0);
return {
count: approved.length,
worstCaseMinor,
expectedLossMinor: Math.round(worstCaseMinor * expectedChargebackRate),
};
}
Worked example with illustrative figures: 4,120 authorizations approved outside normal mode at an average cap of €35 gives €144,200 of worst-case exposure; at a 0.6% expected chargeback rate that is roughly €865. That is the number you bring to the risk committee, not "we kept approving".
Multi-Provider Cloud Architecture Is Not a Resilience Strategy by Itself
The reflex answer is active-active across two hyperscalers. It is expensive and frequently illusory, because the dependencies that actually fail are shared: the same DNS provider, the same identity provider, the same vendor SDK linked into the authorization path, the same third-party risk engine, the same build pipeline that pushed the last bad config to both sides.
What buys independence is independence in the decision path, not the compute path. A fraud-scoring fallback that runs on your own infrastructure, a cached scheme-parameter and BIN table, configuration that resolves locally when the control plane is unreachable.
| Approach | Cost | Complexity | Failure it actually survives |
|---|---|---|---|
| Duplicate the compute | 2× steady-state plus dual-run overhead | High: dual writes, split IAM, drift | Region or AZ loss inside one provider; not a provider-wide control-plane failure, not a shared SDK or vendor bug |
| Duplicate the decision | Low: local fallback scorer, cached tables, local config | Medium: must be tested and reconciled | Loss of the managed dependency itself — which is what usually breaks |
This table is my inference from the architecture, not a claim from the 21 October reports. The reports do not tell us which layer failed.
The Regulatory Layer Now Requires a Documented Degraded-Mode Contract
This stopped being purely an engineering concern. DORA (Regulation (EU) 2022/2554), applicable from 17 January 2025, requires financial entities to manage ICT third-party risk, maintain tested business-continuity arrangements, and report major ICT-related incidents. The UK's critical third-party regime runs in a comparable direction; check its current commencement status rather than relying on this post.
What a supervisor or internal auditor will actually ask for: a documented degraded-mode decision table, evidence that each mode was exercised with dates and results, named authority with an approval trail, and incident timelines measured from the first failed authorization rather than from the first status-page update.
How to Test Your Card Authorization Degraded Mode Before the Next Outage
- Inject 900 ms of latency into the risk-scoring dependency and confirm the deadline still holds and the fallback decision fires.
- Hard-fail the dependency and confirm the breaker opens and stays open across requests.
- Force stand-in by making the issuer endpoint unresponsive; diff the network's decisions against your expected table.
- Drain a region and measure the real evacuation time against the documented RTO — not the one in the slide.
- Verify the reconciliation queue drains and produces a reviewable delta with a euro figure attached.
- Run the authority drill: have the named approver actually exercise the mode switch out of hours, and time how long it takes them to find the credential.
Expect the first run to reveal stand-in parameters nobody reviewed, and at least one fallback path that was documented but never wired.
Conclusion: Own the Degraded-Mode Contract or the Error Handler Will
The failure mode in this incident class is not that a cloud provider went down. It is that the degraded behaviour of the payment path was undefined, so the default — decline — executed. Cloud resilience for regulated payments starts with an owned decision: write the degraded-mode contract, implement it in the authorization path, test the transitions, and reconcile every decision made outside normal mode. Own the decision or the error handler owns it for you.
What would change this analysis: a published Revolut postmortem or a Google Cloud incident report showing the authorization path degraded gracefully and the disruption was confined to non-authorization surfaces. If that appears, the conclusion shifts from "your contract is missing" to "your contract exists but your notification and reconciliation lagged" — a different and much cheaper fix.
Further Reading
- DORA, Regulation (EU) 2022/2554 — EUR-Lex (primary legal text)
- Google Cloud incident and status history (check the entry for 21 October 2026)
- Visa and Mastercard stand-in processing documentation — available through issuer/acquirer portals rather than a stable public URL; ask your acquirer for the current product terms
- Europe's financial regulators query Google over cloud outage that hit banks — AOL.com, 21 October 2026 (secondary coverage)
- Revolut customers find their cards won't work as Google Cloud outage hits banking services — TechTimes.co.uk, 21 October 2026 (secondary coverage)


