When Google Cloud Fails, Card Authorization Needs a Degraded-Mode Contract

When Google Cloud Fails, Card Authorization Needs a Degraded-Mode Contract

pr0h0•
cloud-resiliencefintechcard-paymentsdegraded-mode
AI Usage (76%)

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:

UnknownWhy it mattersWhat would confirm it
Outage duration and affected GCP products/regionsDetermines whether this was a control-plane or data-plane failureA Google Cloud status entry with timestamps
Root causeShared dependency vs. local blast radiusGoogle Cloud incident report
Whether Revolut's authorization path ran on GCP or failed via a downstream processorChanges who owns the degraded modeRevolut postmortem
Declined vs. merely delayed authorizationsA timeout that resolves in 3 s is a different defect from a hard declineScheme/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:

HopTypical p50Worst-case p99Budget allocated
Edge / TLS termination15 ms45 ms50 ms
Auth service dispatch (routing, config lookup)8 ms30 ms40 ms
Risk / fraud scoring90 ms700 ms (then timeout)600 ms max
Ledger / available-balance check40 ms180 ms600 ms
Response write5 ms20 ms250 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:

  1. An explicit mode enumeration — normal, degraded, halted — each with written entry and exit conditions.
  2. A fallback decision per transaction class, not one global policy. Contactless low-value and a high-value premium card are not the same risk.
  3. Named authority to enter and exit a mode, with an approval trail timestamped to the minute.
  4. A reconciliation and audit path for every decision made outside normal mode.
Transaction classDegraded decisionPer-txn capDaily cap (illustrative)
Contactless low-valueApprove with cap€20€60 per card
E-commerce CNPStep-up, else decline€50€150 per card
ATM cashApprove with cap€100€200 per card
Balance inquiryServe cached——
High-value / premiumStand-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
// 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:

lab-harness.js
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.

ApproachCostComplexityFailure it actually survives
Duplicate the compute2× steady-state plus dual-run overheadHigh: dual writes, split IAM, driftRegion or AZ loss inside one provider; not a provider-wide control-plane failure, not a shared SDK or vendor bug
Duplicate the decisionLow: local fallback scorer, cached tables, local configMedium: must be tested and reconciledLoss 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

  1. Inject 900 ms of latency into the risk-scoring dependency and confirm the deadline still holds and the fallback decision fires.
  2. Hard-fail the dependency and confirm the breaker opens and stays open across requests.
  3. Force stand-in by making the issuer endpoint unresponsive; diff the network's decisions against your expected table.
  4. Drain a region and measure the real evacuation time against the documented RTO — not the one in the slide.
  5. Verify the reconciliation queue drains and produces a reviewable delta with a euro figure attached.
  6. 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

Share this post

More posts

Comments