<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feed/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Mohammad Abu Mattar | Incidents</title><description>The latest Incidents from Mohammad Abu Mattar.</description><link>https://mkabumattar.com/</link><item><title>Two Outages Where We Told Envoy Something We Did Not Mean</title><link>https://mkabumattar.com/incidents/post/mesh-config-ordering-outages/</link><guid isPermaLink="true">https://mkabumattar.com/incidents/post/mesh-config-ordering-outages/</guid><description>A service mesh caused two outages, both self-inflicted and both configuration rather than software: a VirtualService applied before its DestinationRule, and a PeerAuthentication scoped to the wrong namespace.</description><pubDate>Mon, 26 Oct 2026 19:05:56 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;Did the sidecars cause outages? Two, both self-inflicted, and both
configuration rather than software. One was a &lt;code&gt;DestinationRule&lt;/code&gt; ordering
mistake. The other was a &lt;code&gt;PeerAuthentication&lt;/code&gt; scoped to the wrong namespace,
which enforced strict mTLS against a service whose caller was not yet
meshed. Neither was Envoy failing; both were us telling it something we did
not mean.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Every request routed to an undefined subset failed, on the first outage.
The error names the destination service, so the alert fired on
&lt;code&gt;payments&lt;/code&gt;, the on-call engineer paged the payments team, and the payments
team correctly reported that they were fine.&lt;/li&gt;
&lt;li&gt;A service whose caller was not yet meshed lost its callers, on the
second, when strict mTLS arrived in a namespace it did not belong in.&lt;/li&gt;
&lt;li&gt;Before the mesh existed at all, the diagnosis cost was its own incident
class: finding which hop was slow took hours, sometimes a day, because
each team had instrumented their own service differently, or not at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;No clock for either outage; both were short, caught by their own blast
radius, and reverted. What the record holds is the mechanism, and both
mechanisms are deterministic enough to deserve the space:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Outage one:&lt;/strong&gt; a &lt;code&gt;VirtualService&lt;/code&gt; (&amp;quot;send 5% to subset v2&amp;quot;) applied before
the &lt;code&gt;DestinationRule&lt;/code&gt; that defines what &lt;code&gt;v2&lt;/code&gt; means. Every request routed
to the undefined subset fails immediately.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Outage two:&lt;/strong&gt; a &lt;code&gt;PeerAuthentication&lt;/code&gt; scoped to the wrong namespace,
enforcing strict mTLS against a service whose caller was not yet meshed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Configuration expressed real intent poorly. The &lt;code&gt;VirtualService&lt;/code&gt; and the
&lt;code&gt;DestinationRule&lt;/code&gt; are two halves of one routing decision that Kubernetes
happily applies in either order, and only one order is correct. The
namespace scope on the &lt;code&gt;PeerAuthentication&lt;/code&gt; was written once and read
everywhere it mattered.&lt;/p&gt;
&lt;h2&gt;Contributing factors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;mTLS arrived by accumulation in reverse.&lt;/strong&gt; The project started because an
auditor asked how we encrypted traffic between services and the honest
answer was that we mostly did not: everything inside the VPC talked over
plain HTTP, roughly 15% encrypted before the rollout.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The migration had no owner for three months.&lt;/strong&gt; The mesh went un-upgraded
through two releases while it sat on the platform team&amp;#39;s list somewhere
below everything with a deadline. A mesh with no owner becomes the thing
every team blames when something is slow and nobody has the standing to
change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retries at two layers.&lt;/strong&gt; We already had retries and timeouts in
application code. Adding them at the mesh layer meant that during one
incident a request was being retried three times by the sidecar and twice
by the caller, turning a slow dependency into a hammered one.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;The full rollout is the case study: eleven weeks of &lt;code&gt;PERMISSIVE&lt;/code&gt; mode,
flipped to &lt;code&gt;STRICT&lt;/code&gt; only after a daily plain-text check had returned
nothing for five consecutive days. The incident-specific changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Apply the &lt;code&gt;DestinationRule&lt;/code&gt; first, always.&lt;/strong&gt; The ordering rule is now
written into the rollout procedure, not carried in one person&amp;#39;s head.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Namespace scoping reviewed per policy&lt;/strong&gt;, not inherited.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mesh-level retries removed; the code-level ones kept.&lt;/strong&gt; One layer
owns retry behaviour.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ownership named before the next migration&lt;/strong&gt;, after three months
proved what ownerless infrastructure decays into.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Did it hold&lt;/h2&gt;
&lt;p&gt;Flipping &lt;code&gt;PERMISSIVE&lt;/code&gt; to &lt;code&gt;STRICT&lt;/code&gt; across everything was a one-line change
and a non-event, because the telemetry had been checked first. Encrypted
service-to-service traffic went from roughly 15% to 100%. Time to identify
a slow hop went from hours, sometimes a day, to minutes, measured against
four incidents after the rollout. Seven canary-caught regressions in six
weeks never became incidents at all.&lt;/p&gt;
&lt;h2&gt;What I would do differently&lt;/h2&gt;
&lt;p&gt;Treat config ordering as part of the interface. &amp;quot;Two objects that only
work in one order&amp;quot; is a design smell in any system, and in a mesh it is an
outage with a two-week fuse.&lt;/p&gt;
&lt;p&gt;The full rollout, the mTLS migration path, and the measured numbers are
written up in
&lt;a href=&quot;/case-studies/post/service-mesh-rollout-when-worth-it&quot;&gt;Service Mesh Rollout: When It Is Worth It&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Reliability</category><category>Kubernetes</category><author>Mohammad Abu Mattar</author></item><item><title>Three Weeks of &quot;Sometimes Doesn&apos;t Load&quot;, While p50 Stayed Green</title><link>https://mkabumattar.com/incidents/post/p99-tail-latency-three-week-blind-spot/</link><guid isPermaLink="true">https://mkabumattar.com/incidents/post/p99-tail-latency-three-week-blind-spot/</guid><description>A largest-account customer&apos;s dashboard intermittently failed to load for about three weeks while the p50 dashboard showed green. p99 was 4.2 seconds against a 90ms median, driven by a nine-table join running 214 times per request.</description><pubDate>Sun, 18 Oct 2026 16:25:54 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The complaint that started this was not about speed. It was a support
ticket saying the dashboard &amp;quot;sometimes doesn&amp;#39;t load&amp;quot;, from a customer whose
account was roughly forty times larger than our median account. Our latency
dashboard showed a p50 of 90ms and a green tick, so for about three weeks
nobody believed them.&lt;/p&gt;
&lt;p&gt;They were right, and our monitoring was lying by omission.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;A real customer, on one of our largest accounts, intermittently unable to
load their dashboard for about three weeks.&lt;/li&gt;
&lt;li&gt;The p99 sat at 4.2 seconds against a p50 of 90ms. That 46x spread is not
a uniformly slow system; it is a small class of requests doing something
completely different from the rest.&lt;/li&gt;
&lt;li&gt;On the account that complained, the largest-account p99 reached 9.8
seconds.&lt;/li&gt;
&lt;li&gt;Large-account requests thrashed Aurora&amp;#39;s buffer pool, so they slowed
everyone else down too.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;No dated clock survives. The shape of the three weeks does:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A support ticket arrives: the dashboard &amp;quot;sometimes doesn&amp;#39;t load&amp;quot;.&lt;/li&gt;
&lt;li&gt;The p50 dashboard shows green, and for about three weeks the ticket
loses to the graph every time it is looked at.&lt;/li&gt;
&lt;li&gt;Someone finally slices by percentile and account. The p99 is 4.2s, and
on the complaining account the endpoint runs its join 214 times and
takes just over four seconds.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;From our own postmortem, three weeks in: &amp;quot;We spent the first sprint
optimising the wrong thing, because the p50 said the system was fine and
the p50 is what the dashboard showed by default.&amp;quot;&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;The offender was &lt;code&gt;GET /api/v1/projects&lt;/code&gt;, which built each row from a
nine-table join covering the project, its owner, its current status, its
two most recent activity entries, and a permissions check. On an account
with three projects it ran the join three times and returned in 60ms. On
the account that complained it ran 214 times.&lt;/p&gt;
&lt;p&gt;The join included a correlated subquery against the activity table, which
had grown past 400 million rows, so its cost scaled with account age rather
than account size.&lt;/p&gt;
&lt;h2&gt;Contributing factors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The dashboard defaulted to p50.&lt;/strong&gt; The median of a bimodal distribution
describes neither mode. Three weeks of disbelief came from one default.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cache keys were built wrong before they were built right.&lt;/strong&gt; The first
caching implementation keyed on the incoming request, including a header
the load balancer sets per request. Hit ratio zero, Redis full of
single-use entries, and a 4% &amp;quot;improvement&amp;quot; that was noise. It took two
weeks and a colleague asking why key cardinality exceeded the user count
to catch it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;KEYS&lt;/code&gt; in production.&lt;/strong&gt; The first invalidation implementation used
&lt;code&gt;KEYS project:*&lt;/code&gt;, which blocks Redis while it scans every key. It worked
in staging with 400 keys and caused a 900ms stall in production with 2
million.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;The full rearchitecture is the case study: CDN and Redis layers in front of
the join, with measured before-and-after numbers. The incident-specific
changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Percentile and account-size slicing in the dashboards&lt;/strong&gt;, so a
large-account tail cannot hide inside a green median again.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key cardinality tracked against expected cardinality&lt;/strong&gt; as a dashboard
panel; it would have caught the cache-key bug on day one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hard rule from the incident review&lt;/strong&gt;: no change could increase the
blast radius of an Aurora failover.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No pattern-based deletion anywhere&lt;/strong&gt;, &lt;code&gt;SCAN&lt;/code&gt;-based invalidation from a
centralised key map instead, because every early staleness bug was a
write path that forgot one of the three keys it should have touched.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Did it hold&lt;/h2&gt;
&lt;p&gt;Measured four weeks before and after at comparable traffic: p99 4.2s to
1.6s, largest-account p99 9.8s to 2.1s, Aurora read IOPS down 71%. The
caveat the review made us write down: caching moved the failure mode, it
did not remove it. Two of three post-rollout incidents were correctness
bugs, and neither triggered a latency alert, because both were fast.&lt;/p&gt;
&lt;h2&gt;What I would do differently&lt;/h2&gt;
&lt;p&gt;Put the tail on the dashboard before a customer has to put it in a ticket.
The p50 default was not a monitoring decision; it was a default.&lt;/p&gt;
&lt;p&gt;The full rearchitecture, including the stale-on-error design that turned an
Aurora failover into slightly old data, is written up in
&lt;a href=&quot;/case-studies/post/p99-latency-caching-rearchitecture&quot;&gt;A p99 Latency Caching Rearcitecture&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Performance</category><category>Observability</category><author>Mohammad Abu Mattar</author></item><item><title>Three Dashboards Reported Zero Revenue, and Every Job Exited 0</title><link>https://mkabumattar.com/incidents/post/cron-silent-zero-revenue-failure/</link><guid isPermaLink="true">https://mkabumattar.com/incidents/post/cron-silent-zero-revenue-failure/</guid><description>A vendor changed an SFTP filename. One cron job silently produced a zero-row file, six downstream jobs consumed it happily, and by morning three dashboards reported zero revenue. Nothing alerted, because nothing had technically failed.</description><pubDate>Wed, 07 Oct 2026 19:41:35 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The failure mode that finally forced the pipeline rewrite was mundane. A
vendor changed an SFTP filename. One job silently produced a zero-row file,
six downstream jobs consumed that file happily, and by morning three
internal dashboards were confidently reporting zero revenue.&lt;/p&gt;
&lt;p&gt;Nothing alerted, because nothing had technically failed. Every job exited 0.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Three internal dashboards reporting zero revenue, believed by everyone who
opened them, because the number looked like a number.&lt;/li&gt;
&lt;li&gt;Nine overnight pages in the eight weeks before this incident class was
retired, and four silent failures found only after the fact.&lt;/li&gt;
&lt;li&gt;Median time to identify a failing job was about 35 minutes, and that was
when someone already knew a job had failed. The first we usually heard of
an overnight break was a customer asking why their report was empty.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;There is no incident clock. The record that survives is the shape of the
night: the zero-row file was produced overnight, consumed by six downstream
jobs, and discovered in the morning by people reading dashboards, not by
any alert.&lt;/p&gt;
&lt;p&gt;What is established about the surrounding regime, because it was every
night, not one night:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;jobs chained by &lt;code&gt;sleep 900&lt;/code&gt;, scheduled by hope: job A at 02:00, job B at
02:15, because A &amp;quot;usually takes ten minutes&amp;quot;, until A took eighteen and
B read a half-written file, which had happened at least twice before&lt;/li&gt;
&lt;li&gt;a failed job at 02:14 was simply not done; several were unsafe to run by
hand twice, which nobody had documented, so re-running was a small act of
faith&lt;/li&gt;
&lt;li&gt;output went to &lt;code&gt;/var/log/cron&lt;/code&gt; on whichever of 6 hosts had drawn the job,
rotated weekly, with no record of normal runtimes&lt;/li&gt;
&lt;li&gt;of the 41 jobs, we could name a current owner for 12&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Two causes, one on each side of the interface.&lt;/p&gt;
&lt;p&gt;The vendor changed a filename without a contract that made that impossible
to do quietly. And our jobs treated an empty input as success: a zero-row
file was processed completely, exited 0, and passed the emptiness
downstream, where six more jobs did the same.&lt;/p&gt;
&lt;h2&gt;Contributing factors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;No retries existed.&lt;/strong&gt; A failed job was missing until run by hand; the
retry story after the migration took a week of chasing duplicate side
effects to get right, because retry and idempotency have to be designed
together.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Detection depended on customers.&lt;/strong&gt; With no alerting, no runtime history,
and no owners, the discovery path for a broken overnight job was a customer
asking about an empty report.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Forty minutes to find a host.&lt;/strong&gt; From the handover: &amp;quot;The thing I remember
is spending forty minutes just working out which host a job ran on. Not
fixing it. Finding it.&amp;quot;&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;The full rewrite is the case study: 41 cron jobs across 6 hosts became 3
scheduled jobs over an SQS-driven event pipeline. The pieces this incident
directly demanded:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Empty is a failure.&lt;/strong&gt; A zero-row input trips an alarm instead of
exiting 0.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retries with idempotency keys&lt;/strong&gt;, so a retry is the same work, not new
work. The first key design was wrong (it included a timestamp) and
produced duplicate PDFs and a very confusing morning; the fixed version
later caught a resurrected cron entry on its own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The alarm moved to DLQ depth&lt;/strong&gt;, which cut overnight pages more than
any reliability improvement did: 9 in 8 weeks before, 2 in 12 weeks
after.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Did it hold&lt;/h2&gt;
&lt;p&gt;The two remaining pages were both genuine: a vendor served malformed data
for two days, and the DLQ caught every affected message. A commented-out
cron entry, uncommented by accident six weeks later, re-ran an already
migrated job, and the idempotency layer absorbed the duplicate it was built
for. Silent failures after the fact: four before, zero after.&lt;/p&gt;
&lt;h2&gt;What I would do differently&lt;/h2&gt;
&lt;p&gt;Delete rather than disable. The old cron entries left commented out &amp;quot;just
in case&amp;quot; were a live footgun for six weeks, and one of them fired.&lt;/p&gt;
&lt;p&gt;The full migration, including the visibility-timeout lesson and the
measured before-and-after, is written up in
&lt;a href=&quot;/case-studies/post/cron-to-event-driven-pipeline&quot;&gt;From 41 Cron Jobs to an Event-Driven Pipeline&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Reliability</category><category>Data Engineering</category><author>Mohammad Abu Mattar</author></item><item><title>The Admission Gate Denied an Image Built from a Fork</title><link>https://mkabumattar.com/incidents/post/slsa-admission-gate-fork-built-image/</link><guid isPermaLink="true">https://mkabumattar.com/incidents/post/slsa-admission-gate-fork-built-image/</guid><description>An image built from a fork by a misconfigured pipeline in a sandbox account reached a production cluster and was denied at admission. Under the old setup it would have run, and nobody would have known.</description><pubDate>Sun, 20 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;The enforcement phase of a supply-chain hardening program denied 23 pods in
its first six weeks. Most were rollout tail: twenty stale base images, two
images built by hand on a developer laptop. The last one was a genuine
incident.&lt;/p&gt;
&lt;p&gt;An image in a production-bound pod had been built from a fork by a
misconfigured pipeline in a sandbox account. Nothing about it was malicious.
The admission check denied it because it could not prove the image was built
by the pipeline, from the commit it claimed, which is exactly the question
the gate exists to ask.&lt;/p&gt;
&lt;p&gt;Under the previous setup, that image runs, and nobody finds out.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;No user-facing impact, which is the point of catching it at admission rather
than after. The real exposure was what the old system could not see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;builds ran on long-lived shared EC2 runners, where a job could read files
another job left behind&lt;/li&gt;
&lt;li&gt;registry credentials were static access keys stored as CI variables,
rotated when someone remembered, and a compromised build in a low-value
repo held the same credentials as everything else&lt;/li&gt;
&lt;li&gt;deployments referenced mutable tags, so anyone with push access could
change what &lt;code&gt;prod&lt;/code&gt; meant on any cluster, with no review&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;There is no clock, and there would not have been one under the old setup
either. The detection method was the gate itself: the pod was denied at
admission, the denial was logged, and the 20/2/1 breakdown of all 23
denials comes from those admission logs.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;The image came from a misconfigured pipeline in a sandbox account, building
from a fork of the repository rather than the repository. Nothing in the
old deployment path would have surfaced that: the manifest named a tag, the
registry had the tag, and the cluster pulled it.&lt;/p&gt;
&lt;h2&gt;Contributing factors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Two builds of the same tag were not the same image.&lt;/strong&gt; While investigating
something unrelated, we found a 40MB difference between two builds of the
same tag, a week apart. Dependencies resolved at build time from public
registries by floating version range. An image with no stable identity
cannot have a provenance story worth checking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Nobody could answer the auditor&amp;#39;s question.&lt;/strong&gt; The program started from a
fair question: can you prove the container running in production is the one
your pipeline built from the commit you think it was? We could tell a
plausible story. We could not produce evidence.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The blast radius of one credential was everything.&lt;/strong&gt; Static, shared,
rarely rotated: the fork-built image was benign, but the same trust model
would have carried a hostile one just as quietly.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;The full program is the case study: hermetic builds, signed provenance,
digest pinning, and the admission gate. The pieces this incident proved out:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Admission enforcement with telemetry first.&lt;/strong&gt; Two weeks of logging
verdicts and denying nothing found 11 problems that would each have been
a production incident on enforcement day.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducible builds.&lt;/strong&gt; Rebuilding the same commit later produced the
same digest for 52 of 60 services; the eight exceptions embed build
timestamps and are on the backlog.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No static registry credentials.&lt;/strong&gt; Eleven before, zero after, measured
by secret scan.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A documented two-approver bypass.&lt;/strong&gt; Expected to be abused, used twice
in six weeks, both times legitimately during an incident.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Did it hold&lt;/h2&gt;
&lt;p&gt;Six weeks of post-enforcement telemetry kept the measurement honest: images
deployable without provenance went from all to zero, services deploying by
mutable tag from 60 of 60 to 0 of 60, and &amp;quot;which commit is running&amp;quot; went
from a twenty-minute archaeology exercise to one command. The break-glass
procedure was rehearsed twice in game days and used zero times in anger.&lt;/p&gt;
&lt;h2&gt;What I would do differently&lt;/h2&gt;
&lt;p&gt;Trust an image you cannot reproduce. The 40MB divergence was found by
accident while investigating something else; reproducibility should have
been a standing check, not a discovery.&lt;/p&gt;
&lt;p&gt;The full program, including the nine-week rollout and the measured results,
is written up in
&lt;a href=&quot;/case-studies/post/cicd-supply-chain-slsa-level-3&quot;&gt;Hardening a CI/CD Supply Chain to SLSA Level 3&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Security</category><category>CI/CD</category><author>Mohammad Abu Mattar</author></item><item><title>A Regional Control-Plane Failure Took a Payments API Offline</title><link>https://mkabumattar.com/incidents/post/single-region-payments-api-regional-outage/</link><guid isPermaLink="true">https://mkabumattar.com/incidents/post/single-region-payments-api-regional-outage/</guid><description>A money-movement API ran in one AWS region for years. A regional control-plane incident took it offline for hours with nowhere to fail over to. The fix was idempotency first, then a second region.</description><pubDate>Tue, 08 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;A payments API that moves real money had run in a single AWS region for years.
A regional control-plane incident took the whole service offline for a few
hours. There was no second region to fail over to, so there was nothing to do
but wait for the region to come back.&lt;/p&gt;
&lt;p&gt;Recovery time was whatever the provider&amp;#39;s recovery time turned out to be. That
is the finding.&lt;/p&gt;
&lt;h2&gt;Impact&lt;/h2&gt;
&lt;p&gt;Total loss of service for the duration. A payments API fails worse than most
things at the same downtime:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;settlements stuck mid-flight rather than merely delayed&lt;/li&gt;
&lt;li&gt;partners escalating against a system that could not answer them&lt;/li&gt;
&lt;li&gt;an availability conversation to have afterwards, because the product is
regulated&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Timeline&lt;/h2&gt;
&lt;p&gt;There isn&amp;#39;t one, and I am not going to invent it. The engineering record of
the work that followed puts the outage at a few hours but never captured when
we detected it or when it cleared.&lt;/p&gt;
&lt;p&gt;What is established: the region&amp;#39;s control plane degraded, the service had no
second region to move to, and it stayed down until the region recovered. No
mitigation existed on our side. A timeline of our actions would be a list of
things we could not do.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Two separate things, and only one of them was ever ours to fix.&lt;/p&gt;
&lt;p&gt;The trigger was a control-plane incident in the provider&amp;#39;s region. Rare,
outside our control, and not something an architecture review would have
prevented.&lt;/p&gt;
&lt;p&gt;The cause was that we ran in one region with no failover target. When the only
region is unavailable there is no operational response available. No runbook
helps, because every path in it ends at the same unreachable place.&lt;/p&gt;
&lt;h2&gt;Contributing factors&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Years of reliability had been reading as evidence of resilience.&lt;/strong&gt; The
service had been dependable in one region for a long time, and that record was
doing the work of an argument that one region was enough. It was never that.
It only meant the question had not come up yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write paths were not idempotent.&lt;/strong&gt; This did not cause the outage, but it
shaped what a fix could look like. When a region gets shaky, clients, proxies
and queues all start retrying at once. Without idempotency, that retry storm
turns one payment into several, so any failover built on those write paths
would have swapped an outage for double charges.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No recovery objective was written down.&lt;/strong&gt; With no agreed RTO or RPO, there
was no number a single-region design had to answer to.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;The mandate was to survive the loss of an entire region without losing a
committed payment or charging anyone twice. In order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Idempotency keys in a globally replicated store&lt;/strong&gt;, so a retry returns the
original result instead of performing the operation again. This came first
on purpose. The real problem was never &amp;quot;run in two regions&amp;quot;, it was &amp;quot;make
every money-touching operation safe to repeat, then run in two regions.&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Active-active across two regions&lt;/strong&gt; rather than a warm standby, so normal
traffic exercises the failover path instead of an emergency being the first
time anyone tries it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Aurora Global Database&lt;/strong&gt; with sub-second replication and a promotable
writer in each region. It fit the EKS and Aurora PostgreSQL the team
already ran, and data-residency rules ruled out a single global write
master.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A written RTO and RPO, proven monthly&lt;/strong&gt;, by deliberately failing a region
out under production-like conditions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On-call needed a failover they could trust at 3am without manual database
promotion steps, because manual steps under pressure are how a recoverable
incident becomes a data-loss incident. That constraint is why the failover is
automatic rather than a runbook.&lt;/p&gt;
&lt;h2&gt;Did it hold&lt;/h2&gt;
&lt;p&gt;In the quarter after cutover the primary region degraded twice. Both times
traffic moved to the healthy region inside the target window, with no
customer-visible errors and no duplicate settlements.&lt;/p&gt;
&lt;p&gt;Finance and risk watched one number, the double-charge count, and it stayed at
zero. Failures kept happening. Every write path had become safe to repeat.&lt;/p&gt;
&lt;h2&gt;What I would do differently&lt;/h2&gt;
&lt;p&gt;Write the recovery objective down before you need it. The missing RTO was not
a documentation gap. It was the reason nobody ever had to defend running in
one region, and a number on paper forces that conversation while there is
still time to have it.&lt;/p&gt;
&lt;p&gt;Read a long clean record as the weak evidence it is. Years of uptime in one
region tells you nothing about losing that region. It tells you the question
has not been asked.&lt;/p&gt;
&lt;p&gt;The full build, including the architecture, the idempotent request flow, the
failover timeline and what broke during the game-days, is written up in
&lt;a href=&quot;/case-studies/post/multi-region-active-active-payments&quot;&gt;Multi-Region Active-Active for a Payments API&lt;/a&gt;.&lt;/p&gt;
</content:encoded><category>Reliability</category><category>Architecture</category><author>Mohammad Abu Mattar</author></item></channel></rss>