<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: binadit</title>
    <description>The latest articles on DEV Community by binadit (@binadit).</description>
    <link>https://dev.to/binadit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3853937%2F7b742322-ef72-44c9-92e2-8a32b6f3aa67.png</url>
      <title>DEV Community: binadit</title>
      <link>https://dev.to/binadit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/binadit"/>
    <language>en</language>
    <item>
      <title>From 4.2s to 380ms: debugging latency in a high availability infrastructure setup</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 11 Sep 2026 07:05:45 +0000</pubDate>
      <link>https://dev.to/binadit/from-42s-to-380ms-debugging-latency-in-a-high-availability-infrastructure-setup-5gj</link>
      <guid>https://dev.to/binadit/from-42s-to-380ms-debugging-latency-in-a-high-availability-infrastructure-setup-5gj</guid>
      <description>&lt;h1&gt;
  
  
  Five silent bottlenecks that turned a 400ms API into a 4.2s crawl
&lt;/h1&gt;

&lt;p&gt;No deploys. No schema changes. No new integrations. Just a scheduling and resource-planning SaaS with 40k users watching its p95 response time climb from 400ms to 4.2 seconds over six months. Nobody could point to a cause, because there wasn't one big cause, there were five small ones stacked on top of each other.&lt;/p&gt;

&lt;p&gt;This is the writeup of how we found and fixed them, without a rearchitecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptoms before the numbers existed
&lt;/h2&gt;

&lt;p&gt;Support tickets called the dashboard "laggy" long before anyone had hard metrics. By the time it got measured:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;p95 latency: 400ms to 4.2s over ~6 months&lt;/li&gt;
&lt;li&gt;Trial-to-paid conversion down 11% in the same window&lt;/li&gt;
&lt;li&gt;Sales getting asked about performance during renewals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The team had already tried the usual moves: bigger instances, a read replica, scheduled restarts. None of it held. When scaling vertically twice does nothing, that's a strong signal the problem is architectural, not capacity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: measure before touching anything
&lt;/h2&gt;

&lt;p&gt;We spent the first week purely on instrumentation, tracing requests across the API gateway, app servers, database, and cache layer under normal load. No fixes yet. Changing the system while you're trying to measure it just adds noise.&lt;/p&gt;

&lt;p&gt;Five issues surfaced. None of them alone explained 4.2 seconds, but together they did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five issues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. N+1 queries in a hot path
&lt;/h3&gt;

&lt;p&gt;A dashboard endpoint fetched a customer's project list, then hit the DB once per project for status. 40 projects meant 41 round trips for one page load.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Connection pool exhaustion
&lt;/h3&gt;

&lt;p&gt;8 app servers x 20 connections each = 160 possible connections, against a Postgres &lt;code&gt;max_connections&lt;/code&gt; of 100. At peak, requests queued for a connection slot before a query even ran.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cache invalidation nuking whole namespaces
&lt;/h3&gt;

&lt;p&gt;Redis had a 30s TTL on project status, reasonable on paper. But a background job flushed entire cache namespaces on &lt;em&gt;any&lt;/em&gt; write, including unrelated ones. Actual hit ratio: 34%.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cross-AZ chatter
&lt;/h3&gt;

&lt;p&gt;App tier and Redis cluster weren't pinned to the same AZ. ~40% of Redis calls crossed zones, adding 2 to 4ms each. At volume, that's hundreds of milliseconds per request.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A synchronous third-party call in the request path
&lt;/h3&gt;

&lt;p&gt;A usage-tracking webhook to an external analytics provider ran synchronously inside the request cycle. On a bad day that provider took 800ms to 1.5s, and every request waited on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we prioritized fixes
&lt;/h2&gt;

&lt;p&gt;Impact vs. risk, not ease of implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sync third-party call, high impact, low risk&lt;/li&gt;
&lt;li&gt;N+1 queries, high impact, low risk&lt;/li&gt;
&lt;li&gt;Connection pool exhaustion, high impact, medium risk&lt;/li&gt;
&lt;li&gt;Cache invalidation, medium impact, medium risk&lt;/li&gt;
&lt;li&gt;Cross-AZ placement, medium impact, low risk&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We explicitly skipped touching instance sizes again. Two rounds of vertical scaling with zero improvement was itself the data point: the bottleneck was I/O and architecture, not compute.&lt;/p&gt;

&lt;p&gt;We also shipped fixes one at a time, measuring after each. Bundling everything into one release makes it impossible to know what actually helped, or what quietly broke something else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Move the analytics call off the request path
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before: synchronous call blocking the response&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;analyticsClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 800ms-1.5s on slow days&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// after: fire-and-forget via queue&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analytics.track&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ~2ms&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Used the existing Redis infra as the queue backend, no new dependency. A worker process handles delivery with retries and a dead-letter queue. This alone cut 800ms to 1.5s off the median request on affected endpoints, and when the analytics provider had two outages during the engagement, users never noticed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kill the N+1
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- before&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- then per project:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;project_status&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- after&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updated_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;projects&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;project_status&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;account_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;41 round trips became 1. Query time for that endpoint went from ~620ms to ~45ms average.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retune the connection pool
&lt;/h3&gt;

&lt;p&gt;Dropped per-server pool size from 20 to 12 (8 x 12 = 96, under the 100 limit) and added PgBouncer in transaction pooling mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;app_db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=5432 dbname=app_production&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;pool_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;transaction&lt;/span&gt;
&lt;span class="py"&gt;max_client_conn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;
&lt;span class="py"&gt;default_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;25&lt;/span&gt;
&lt;span class="py"&gt;reserve_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;reserve_pool_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Connection wait time went from ~180ms average at peak to under 5ms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix cache invalidation
&lt;/h3&gt;

&lt;p&gt;Switched from namespace-wide flushes on any write to key-level invalidation tied to the specific project changed. Kept the 30s TTL as a safety net, but it stopped being the primary mechanism.&lt;/p&gt;

&lt;p&gt;Cache hit ratio: 34% to 91% within a week. Small diff, outsized effect on database load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;None of these fixes were exotic. This is what accumulated technical debt looks like in a system that grew for a few years without a dedicated performance pass. If your latency crept up gradually with no single obvious cause, look for a stack of small architectural issues before you reach for bigger hardware.&lt;/p&gt;

&lt;p&gt;Full writeup with more detail: &lt;a href="https://binadit.com/blog/debugging-latency-high-availability-infrastructure-case-study" rel="noopener noreferrer"&gt;Debugging latency in a high availability infrastructure setup&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/debugging-latency-high-availability-infrastructure-case-study" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>debugging</category>
      <category>infrastructure</category>
      <category>performance</category>
    </item>
    <item>
      <title>How Docker networking broke checkout under load: a container ecommerce infrastructure case study</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 09 Sep 2026 07:02:54 +0000</pubDate>
      <link>https://dev.to/binadit/how-docker-networking-broke-checkout-under-load-a-container-ecommerce-infrastructure-case-study-4cja</link>
      <guid>https://dev.to/binadit/how-docker-networking-broke-checkout-under-load-a-container-ecommerce-infrastructure-case-study-4cja</guid>
      <description>&lt;h2&gt;
  
  
  Checkout was timing out at 900 req/s, and it had nothing to do with CPU
&lt;/h2&gt;

&lt;p&gt;A marketplace client came to us with a scary but familiar symptom: checkout p95 latency jumping from 280ms to over 2.1 seconds during traffic spikes. Their instinct was to throw more containers at it. That made things &lt;em&gt;worse&lt;/em&gt;. Here's what was actually going on, and how we fixed it without touching a line of application code.&lt;/p&gt;

&lt;p&gt;Link to the full writeup: &lt;a href="https://binadit.com/blog/docker-networking-production-ecommerce-infrastructure-case-study" rel="noopener noreferrer"&gt;Docker networking broke checkout under load&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The setup
&lt;/h3&gt;

&lt;p&gt;PHP monolith, containerized about a year prior, running on a single host via Docker Compose: app containers, Redis, a worker queue, behind a managed load balancer. 40k DAU, peak ~900 req/s. It ran fine for six months. Then flash sales started producing "the site is freezing" tickets.&lt;/p&gt;

&lt;p&gt;The team's first move was scaling app containers, assuming CPU/memory pressure. Query times on Postgres stayed stable at 8-14ms the whole time, so the database wasn't the culprit either. The bottleneck was hiding in the network layer between containers, a place default tooling barely gives you visibility into.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the audit found
&lt;/h3&gt;

&lt;p&gt;Three compounding issues, none fatal alone:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Default bridge network overhead.&lt;/strong&gt; Every inter-container hop (app to Redis, app to Postgres, app to search) was going through userland proxying on the default bridge. We measured ~1.8ms added latency per hop. At 900 req/s with 3-4 internal calls per request, that adds up fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Conntrack table exhaustion.&lt;/strong&gt; &lt;code&gt;nf_conntrack_max&lt;/code&gt; was still at the kernel default of 65,536. Short-lived Redis/Postgres connections churned through the table during spikes, filled it, and the kernel silently started dropping packets. Nobody noticed because syslog wasn't shipped anywhere useful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. DNS resolution overhead.&lt;/strong&gt; Docker's embedded DNS (127.0.0.11) was resolving service names on every new connection instead of the app caching results. Under load, with connection churn, lookups started queuing behind each other.&lt;/p&gt;

&lt;p&gt;None of these show up with 10 test users in staging. All three show up hard with 900 real ones under concurrent load.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we didn't do
&lt;/h3&gt;

&lt;p&gt;We ruled out "just move to Kubernetes." It wouldn't have fixed anything here; K8s has its own version of the same problems (CNI choice, kube-proxy mode, CoreDNS caching). Swapping orchestrators without fixing the root cause just relocates it.&lt;/p&gt;

&lt;p&gt;We also ruled out rewriting the app to reduce internal calls. The call pattern was normal. The network layer needed to handle it efficiently, not the other way around.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix, in four layers
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Kernel tuning for conntrack:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;.&lt;span class="n"&gt;netfilter&lt;/span&gt;.&lt;span class="n"&gt;nf_conntrack_max&lt;/span&gt; = &lt;span class="m"&gt;262144&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;.&lt;span class="n"&gt;netfilter&lt;/span&gt;.&lt;span class="n"&gt;nf_conntrack_tcp_timeout_established&lt;/span&gt; = &lt;span class="m"&gt;600&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;.&lt;span class="n"&gt;ipv4&lt;/span&gt;.&lt;span class="n"&gt;tcp_tw_reuse&lt;/span&gt; = &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;.&lt;span class="n"&gt;core&lt;/span&gt;.&lt;span class="n"&gt;somaxconn&lt;/span&gt; = &lt;span class="m"&gt;4096&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Applied via &lt;code&gt;/etc/sysctl.d/99-docker-network.conf&lt;/code&gt; and &lt;code&gt;sysctl --system&lt;/code&gt;. We also started monitoring conntrack table utilization going forward, since a full table fails silently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Moved internal traffic off the default bridge:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--driver&lt;/span&gt; bridge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--opt&lt;/span&gt; com.docker.network.bridge.enable_icc&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--opt&lt;/span&gt; com.docker.network.driver.mtu&lt;span class="o"&gt;=&lt;/span&gt;9000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet&lt;/span&gt; 172.28.0.0/16 &lt;span class="se"&gt;\&lt;/span&gt;
  internal-services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App containers, Redis, and the search sidecar moved onto this network, keeping east-west traffic off Docker's default NAT path. The public-facing load balancer stayed on a separate network; no change to external attack surface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local DNS caching with dnsmasq as a sidecar:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# dnsmasq.conf
&lt;/span&gt;&lt;span class="n"&gt;no&lt;/span&gt;-&lt;span class="n"&gt;resolv&lt;/span&gt;
&lt;span class="n"&gt;server&lt;/span&gt;=&lt;span class="m"&gt;127&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;11&lt;/span&gt;
&lt;span class="n"&gt;cache&lt;/span&gt;-&lt;span class="n"&gt;size&lt;/span&gt;=&lt;span class="m"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;local&lt;/span&gt;-&lt;span class="n"&gt;ttl&lt;/span&gt;=&lt;span class="m"&gt;10&lt;/span&gt;
&lt;span class="n"&gt;neg&lt;/span&gt;-&lt;span class="n"&gt;ttl&lt;/span&gt;=&lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 10-second TTL absorbed connection churn during spikes without causing stale resolution issues when containers got replaced on deploy. We specifically tested that a container restart got picked up within one TTL window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PgBouncer for connection pooling:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;marketplace&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=postgres-primary port=5432 dbname=marketplace&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;pool_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;transaction&lt;/span&gt;
&lt;span class="py"&gt;max_client_conn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2000&lt;/span&gt;
&lt;span class="py"&gt;default_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was sequenced last on purpose. Fewer short-lived connections means fewer conntrack entries and fewer DNS lookups, so it made everything upstream easier once the network layer was already sound.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;If your containerized app slows down only under load and CPU/memory graphs look fine, stop scaling containers and go look at conntrack, your bridge driver, and DNS resolution behavior. That's usually where it's actually hiding.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/docker-networking-production-ecommerce-infrastructure-case-study" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>infrastructure</category>
      <category>networking</category>
      <category>performance</category>
    </item>
    <item>
      <title>Solving the real ROI question behind moving off US hyperscalers</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:23:04 +0000</pubDate>
      <link>https://dev.to/binadit/solving-the-real-roi-question-behind-moving-off-us-hyperscalers-2j8k</link>
      <guid>https://dev.to/binadit/solving-the-real-roi-question-behind-moving-off-us-hyperscalers-2j8k</guid>
      <description>&lt;h1&gt;
  
  
  Why your hyperscaler exit math keeps failing finance review
&lt;/h1&gt;

&lt;p&gt;You run the numbers on leaving AWS. Compute is cheaper elsewhere. Storage is cheaper elsewhere. Then finance asks about egress fees, migration hours, and cutover risk, and the whole business case falls apart. Sound familiar? This isn't a bad migration plan, it's a broken cost model.&lt;/p&gt;

&lt;p&gt;Most engineers comparing hyperscaler costs against alternatives only look at list prices for compute and storage. That's maybe 40% of the real picture. Here's the rest of it, and a framework to fix your ROI math.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the comparison breaks down
&lt;/h2&gt;

&lt;p&gt;Hyperscalers don't really compete on compute price. They compete on making it painful to leave. That's baked into the architecture, not just the contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Egress fees are the real anchor.&lt;/strong&gt; AWS charges roughly $0.09/GB after the first GB out. For a platform pushing 50TB/month (video, API responses, backups, CDN pulls), that's about $4,500/month just to move data, before touching anything else. This fee isn't about bandwidth cost, bandwidth is cheap. It's a moat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Managed services lock you in harder than compute does.&lt;/strong&gt; If you're on RDS, DynamoDB, SQS, and Lambda, you're not renting VMs, you're depending on proprietary APIs. Migrating off means rewriting your data access layer, not just moving instances. This is the line item most spreadsheets underestimate by 3-5x.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reserved instance pricing hides a bad assumption.&lt;/strong&gt; Finance often compares a competitor's list price to a hyperscaler's 3-year reserved rate and calls it a win for the hyperscaler. But that rate assumes flat usage for 36 months. Most SaaS workloads have seasonal spikes and growth curves. A 3-year commitment is a liability wearing a discount's clothes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support cost is invisible until you actually need it.&lt;/strong&gt; A named TAM tier starts around $15,000/month, and you're still in a ticket queue for anything real. Compare that to a partner where a senior engineer just picks up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: five cost categories, not one
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Baseline 12 months of actual spend, not one month
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce get-cost-and-usage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2024-11-01,End&lt;span class="o"&gt;=&lt;/span&gt;2025-11-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--granularity&lt;/span&gt; MONTHLY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metrics&lt;/span&gt; &lt;span class="s1"&gt;'UnblendedCost'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-by&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DIMENSION,Key&lt;span class="o"&gt;=&lt;/span&gt;SERVICE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Break it into compute, storage, egress, managed services, support, and RI amortization. Egress alone is usually 8-15% of total spend for content-heavy platforms, and it almost never makes the first draft of a migration business case.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Model the actual migration cost
&lt;/h3&gt;

&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Engineering hours to replace managed-service dependencies (DynamoDB to Postgres, Lambda to containers, SQS to self-hosted queues)&lt;/li&gt;
&lt;li&gt;One-time data transfer cost to move the dataset out&lt;/li&gt;
&lt;li&gt;Parallel-run cost during validation (typically 4-8 weeks)&lt;/li&gt;
&lt;li&gt;A downtime contingency line, even with a zero-downtime plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a mid-sized SaaS app with a 2TB database and 15 microservices, expect 200-450 engineering hours. At 80 euros/hour loaded cost, that's 16,000-36,000 euros, upfront. Put it in the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Recalculate egress under the new architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# AWS us-east-1 to internet: $0.09/GB after first 1GB free&lt;/span&gt;
&lt;span class="c"&gt;# EU provider with peering to major IXPs: $0.01-0.02/GB typical&lt;/span&gt;

&lt;span class="c"&gt;# 50TB/month egress:&lt;/span&gt;
&lt;span class="c"&gt;# AWS: 50,000GB * $0.09 = $4,500/month&lt;/span&gt;
&lt;span class="c"&gt;# EU provider: 50,000GB * $0.015 = $750/month&lt;/span&gt;
&lt;span class="c"&gt;# Annual difference: $45,000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Colocate with your CDN's regional PoPs and egress typically drops 40-70%.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Swap proprietary APIs for open standards
&lt;/h3&gt;

&lt;p&gt;This is the highest-leverage move in the whole exit. It's what makes the savings durable instead of a one-time discount you slowly erode by re-adopting vendor tooling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DynamoDB → PostgreSQL or self-managed MongoDB&lt;/li&gt;
&lt;li&gt;Lambda → containers on Kubernetes or Nomad&lt;/li&gt;
&lt;li&gt;SQS/SNS → self-hosted RabbitMQ or Kafka&lt;/li&gt;
&lt;li&gt;CloudWatch → Prometheus + Grafana&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Cut over with zero downtime
&lt;/h3&gt;

&lt;p&gt;Replicate continuously, run both environments in parallel, lower DNS TTL ahead of time, keep the old environment warm for at least one billing cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 48-72 hours before cutover&lt;/span&gt;
example.com.  300  IN  A  203.0.113.10

&lt;span class="c"&gt;# Monitor both origins during cutover&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{time_total}s\n'&lt;/span&gt; https://old-origin.example.com/health
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code} %{time_total}s\n'&lt;/span&gt; https://new-origin.example.com/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For database-backed apps, run logical replication for days beforehand, not a single export/import window.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to know it actually worked
&lt;/h2&gt;

&lt;p&gt;Track these for at least two full billing cycles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total spend vs. your 12-month baseline, normalized for traffic growth&lt;/li&gt;
&lt;li&gt;Egress as % of total spend (expect 8-15% → 2-4% with good peering)&lt;/li&gt;
&lt;li&gt;p95/p99 latency on your top 10 endpoints, before and after&lt;/li&gt;
&lt;li&gt;Error rate and uptime, 30 days before vs. 30 days after&lt;/li&gt;
&lt;li&gt;Engineering hours on infra ops per month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One sanity check that matters: if spend drops 30% but p99 latency degrades 25%, you haven't won anything. You've just shifted cost into a metric that'll eventually cost you conversions.&lt;/p&gt;

&lt;p&gt;Full framework and migration mechanics in the original piece: &lt;a href="https://binadit.com/blog/cloud-cost-optimization-services-roi-moving-off-us-hyperscalers" rel="noopener noreferrer"&gt;Solving the real ROI question behind moving off US hyperscalers&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/cloud-cost-optimization-services-roi-moving-off-us-hyperscalers" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>How to move ecommerce infrastructure from a single VPS to HA without rewriting the application</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 26 Aug 2026 07:22:59 +0000</pubDate>
      <link>https://dev.to/binadit/how-to-move-ecommerce-infrastructure-from-a-single-vps-to-ha-without-rewriting-the-application-48h</link>
      <guid>https://dev.to/binadit/how-to-move-ecommerce-infrastructure-from-a-single-vps-to-ha-without-rewriting-the-application-48h</guid>
      <description>&lt;h2&gt;
  
  
  Your VPS will fail at the worst possible time
&lt;/h2&gt;

&lt;p&gt;Here's an uncomfortable truth: that single VPS running your store isn't a matter of if it fails, it's when. A kernel update reboot, a Black Friday memory spike, a disk that fills up overnight. None of these are exotic failure modes, they're Tuesday.&lt;/p&gt;

&lt;p&gt;The good news: fixing this is an infrastructure problem, not a rewrite. If you're running PHP, Node, or Python against a relational database, you can go from one box to a highly available setup without touching your application code. This applies whether you're on WooCommerce, Magento, or something custom.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you touch anything
&lt;/h2&gt;

&lt;p&gt;Check these boxes first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your app can run statelessly across multiple servers (or can be made to, sessions and uploads are the usual culprits)&lt;/li&gt;
&lt;li&gt;You have root, not just FTP&lt;/li&gt;
&lt;li&gt;You've scheduled a maintenance window for DNS and DB cutover&lt;/li&gt;
&lt;li&gt;You have a tested, recent backup&lt;/li&gt;
&lt;li&gt;You know your traffic pattern: peak RPS, DB connections, payload size&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This walkthrough assumes a LAMP/LEMP-ish stack: Nginx or Apache, PHP-FPM or Node, MySQL or Postgres, Redis. Swap tooling as needed for other stacks, the pattern holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Get state off local disk
&lt;/h2&gt;

&lt;p&gt;This is the real blocker to scaling horizontally. Local disk state has to go before you add a second server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sessions&lt;/strong&gt; → move to Redis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;; php.ini
&lt;/span&gt;&lt;span class="py"&gt;session.save_handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
&lt;span class="py"&gt;session.save_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tcp://10.0.0.5:6379"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Uploaded media&lt;/strong&gt; → object storage (S3-compatible) or shared NFS. For WooCommerce, WP Offload Media handles this out of the box. For custom apps, swap filesystem writes for an SDK call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$s3&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;putObject&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'Bucket'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'store-uploads'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Key'&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'Body'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;fopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$tmpPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cache&lt;/strong&gt; → Redis or Memcached instead of local disk/OPcache-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Put a load balancer in front, even with one backend
&lt;/h2&gt;

&lt;p&gt;Spin up a small VM or a managed LB now, before you have a second server. Point DNS at the LB's IP immediately. This decouples DNS from your server count forever, no more DNS changes for future scaling.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app_servers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.0.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;max_fails=3&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=30s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.0.11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;max_fails=3&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=30s&lt;/span&gt; &lt;span class="s"&gt;backup&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;443&lt;/span&gt; &lt;span class="s"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;shop.example.com&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://app_servers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Clone the app server
&lt;/h2&gt;

&lt;p&gt;Once sessions and media are externalized, your app server is basically stateless. Build a second node from the same provisioning script, Ansible, Docker, or a shell script, doesn't matter, just make it reproducible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# minimal provisioning sanity check&lt;/span&gt;
php &lt;span class="nt"&gt;-v&lt;/span&gt;
nginx &lt;span class="nt"&gt;-v&lt;/span&gt;
composer &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/php/8.2/fpm/pool.d/www.conf | &lt;span class="nb"&gt;grep &lt;/span&gt;pm.max_children
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to the upstream block, drop the &lt;code&gt;backup&lt;/code&gt; flag, confirm both nodes handle real traffic before moving to the database layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Replicate the database
&lt;/h2&gt;

&lt;p&gt;Highest-risk step. Set up primary-replica replication or move to a managed cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# primary my.cnf
&lt;/span&gt;&lt;span class="py"&gt;server-id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;log_bin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/mysql-bin.log&lt;/span&gt;
&lt;span class="py"&gt;binlog_do_db&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;shop_production&lt;/span&gt;

&lt;span class="c"&gt;# replica my.cnf
&lt;/span&gt;&lt;span class="py"&gt;server-id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;relay-log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/mysql/mysql-relay-bin.log&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;CHANGE&lt;/span&gt; &lt;span class="n"&gt;MASTER&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'10.0.0.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'replicator'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'***'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_LOG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'mysql-bin.000003'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;MASTER_LOG_POS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;154&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="n"&gt;SLAVE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check lag before cutover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SLAVE&lt;/span&gt; &lt;span class="n"&gt;STATUS&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="k"&gt;G&lt;/span&gt;
&lt;span class="c1"&gt;-- Seconds_Behind_Master should be 0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once stable, cut over writes using a virtual IP or ProxySQL, not a manual config edit during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Real health checks, not port pings
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;app_servers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.0.10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;max_fails=2&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;10.0.0.11&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="s"&gt;max_fails=2&lt;/span&gt; &lt;span class="s"&gt;fail_timeout=10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# haproxy / nginx plus&lt;/span&gt;
&lt;span class="k"&gt;option&lt;/span&gt; &lt;span class="s"&gt;httpchk&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;/health&lt;/span&gt;
&lt;span class="s"&gt;http-check&lt;/span&gt; &lt;span class="s"&gt;expect&lt;/span&gt; &lt;span class="s"&gt;status&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;/health&lt;/code&gt; endpoint needs to actually check dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$pdo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PDO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$dsn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$pass&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$redis&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$redis&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'10.0.0.5'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'ok'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;http_response_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'unhealthy'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prove it actually works
&lt;/h2&gt;

&lt;p&gt;Don't trust the config, test it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Kill a node during low traffic, confirm the LB routes around it inside your &lt;code&gt;fail_timeout&lt;/code&gt; window and response times stay flat&lt;/li&gt;
&lt;li&gt;Watch &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; during peak checkout load, not just idle&lt;/li&gt;
&lt;li&gt;Load test both nodes: &lt;code&gt;ab -n 5000 -c 50 https://shop.example.com/&lt;/code&gt; and diff the access logs&lt;/li&gt;
&lt;li&gt;Simulate a DB failover in staging, measure reconnect time (should be seconds, not minutes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Baseline to aim for: zero customer-visible errors when one app node dies, database failover under 30 seconds with retry logic in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that will bite you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sticky sessions instead of externalized sessions&lt;/strong&gt;: works fine until a node dies and half your logged-in users get booted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate cron jobs&lt;/strong&gt;: order processing and cache warming running on both nodes doubles the work, pin them to one node or use a job queue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shallow health checks&lt;/strong&gt;: pinging port 80 says nothing about whether the DB connection is alive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication as a backup strategy&lt;/strong&gt;: it protects against hardware failure, not against a bad &lt;code&gt;DELETE&lt;/code&gt; that replicates instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never testing failover&lt;/strong&gt;: the first failover shouldn't happen during a real outage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full walkthrough with more context here: &lt;a href="https://binadit.com/blog/single-vps-to-ha-ecommerce-infrastructure-without-rewrite" rel="noopener noreferrer"&gt;How to move ecommerce infrastructure from a single VPS to HA without rewriting the application&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/single-vps-to-ha-ecommerce-infrastructure-without-rewrite" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Measuring what FISA 702 reauthorization actually changes for EU SaaS infrastructure</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:24:39 +0000</pubDate>
      <link>https://dev.to/binadit/measuring-what-fisa-702-reauthorization-actually-changes-for-eu-saas-infrastructure-2nlb</link>
      <guid>https://dev.to/binadit/measuring-what-fisa-702-reauthorization-actually-changes-for-eu-saas-infrastructure-2nlb</guid>
      <description>&lt;h2&gt;
  
  
  Your "EU region" toggle probably isn't doing what you think
&lt;/h2&gt;

&lt;p&gt;Here's an uncomfortable fact: setting your AWS region to &lt;code&gt;eu-central-1&lt;/code&gt; does nothing to shield you from FISA 702. None of it. If your provider is a US company, that data can still be compelled, regardless of which data center it physically sits in. We ran the numbers to find out what it actually takes to fix this, and whether the fix costs you performance.&lt;/p&gt;

&lt;p&gt;FISA 702 got reauthorized in April 2024, running through 2026. It'll come up again, and every time it does, compliance teams ask engineering the same question: can we safely keep running on US-owned cloud infra? The answer isn't legal, it's architectural.&lt;/p&gt;

&lt;h3&gt;
  
  
  The setup
&lt;/h3&gt;

&lt;p&gt;We benchmarked the same Laravel API workload (PostgreSQL 16, Redis 7, object storage) across three configs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Config A&lt;/strong&gt;: AWS &lt;code&gt;eu-central-1&lt;/code&gt;, standard hyperscaler, US-headquartered&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config B&lt;/strong&gt;: EU-owned provider (OVHcloud), data residency guarantees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Config C&lt;/strong&gt;: Dedicated private cloud, Rotterdam data center, no shared hyperscaler control plane&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same hardware baseline everywhere: 8 vCPU / 32GB app nodes, NVMe DB nodes at 4 vCPU / 16GB, PgBouncer in front of Postgres, Nginx 1.25 as reverse proxy.&lt;/p&gt;

&lt;p&gt;Load generated with k6, from a Frankfurt runner to kill long-haul network noise as a variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// k6 load profile&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// ramp 50 -&amp;gt; 2000 VUs&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;20m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// sustained peak&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="c1"&gt;// mix: 70% GET, 30% POST/PUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We measured two separate things: raw performance, and legal exposure surface (who can be compelled to hand over your data, under what instrument, with or without notifying you).&lt;/p&gt;

&lt;h3&gt;
  
  
  Result 1: performance is basically a wash
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;p50&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;th&gt;Max sustained req/s&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: AWS eu-central-1&lt;/td&gt;
&lt;td&gt;42ms&lt;/td&gt;
&lt;td&gt;118ms&lt;/td&gt;
&lt;td&gt;210ms&lt;/td&gt;
&lt;td&gt;3,150&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: OVHcloud (EU)&lt;/td&gt;
&lt;td&gt;47ms&lt;/td&gt;
&lt;td&gt;134ms&lt;/td&gt;
&lt;td&gt;245ms&lt;/td&gt;
&lt;td&gt;2,890&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: Private cloud (Rotterdam)&lt;/td&gt;
&lt;td&gt;39ms&lt;/td&gt;
&lt;td&gt;102ms&lt;/td&gt;
&lt;td&gt;178ms&lt;/td&gt;
&lt;td&gt;3,020&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The private cloud setup wins on tail latency, but only by 15-20%. If you're picking your infra provider based on speed alone, don't bother, the difference is inside normal variance for most apps. This is not a performance decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Result 2: the legal exposure gap is real and it's structural
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Config&lt;/th&gt;
&lt;th&gt;Subject to FISA 702&lt;/th&gt;
&lt;th&gt;Compellable without notifying you&lt;/th&gt;
&lt;th&gt;Legal instrument&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A: AWS eu-central-1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes, gag orders are standard&lt;/td&gt;
&lt;td&gt;FISA 702 + CLOUD Act&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B: OVHcloud&lt;/td&gt;
&lt;td&gt;No direct exposure, but sub-processor risk&lt;/td&gt;
&lt;td&gt;Depends on your stack&lt;/td&gt;
&lt;td&gt;GDPR only, if fully EU-owned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C: Private cloud&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;GDPR + Dutch law only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the part that should worry you: &lt;strong&gt;60% of the "EU cloud" test deployments we checked were still routing through US-owned CDNs, DNS, or email providers.&lt;/strong&gt; You think you're compliant because your DB lives in an EU region, but your DNS resolver, your CDN, and your transactional email are all silently reintroducing FISA 702 exposure.&lt;/p&gt;

&lt;p&gt;The region setting is not the control point. The corporate parent of every vendor in your chain is.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually costs to fix
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;US-owned default&lt;/th&gt;
&lt;th&gt;EU-owned alternative&lt;/th&gt;
&lt;th&gt;Effort&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DNS&lt;/td&gt;
&lt;td&gt;Route 53&lt;/td&gt;
&lt;td&gt;deSEC / EU-hosted BIND&lt;/td&gt;
&lt;td&gt;Low, 2-4 hrs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDN&lt;/td&gt;
&lt;td&gt;CloudFront&lt;/td&gt;
&lt;td&gt;Bunny CDN (EU entity)&lt;/td&gt;
&lt;td&gt;Medium, 1-2 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;SES / SendGrid&lt;/td&gt;
&lt;td&gt;Mailjet (FR entity)&lt;/td&gt;
&lt;td&gt;Medium, 1 day + DNS propagation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object storage&lt;/td&gt;
&lt;td&gt;S3&lt;/td&gt;
&lt;td&gt;OVHcloud Object Storage / self-hosted MinIO&lt;/td&gt;
&lt;td&gt;High, 3-5 days&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitoring/APM&lt;/td&gt;
&lt;td&gt;Datadog&lt;/td&gt;
&lt;td&gt;Self-hosted Grafana + Prometheus&lt;/td&gt;
&lt;td&gt;High, 1-2 weeks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the pattern: cheap fixes first, expensive fixes require real migration planning.&lt;/p&gt;

&lt;h3&gt;
  
  
  The pragmatic order of operations
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Swap DNS and CDN first.&lt;/strong&gt; Cheap, fast, immediately reduces exposure. A few hours to a couple days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move transactional email next.&lt;/strong&gt; Slightly more friction from DNS propagation, still low-risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat object storage as a proper migration, not a cutover.&lt;/strong&gt; This one involves real data transfer volume. Plan it with a testing window, not a weekend deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-host monitoring last&lt;/strong&gt;, or accept the risk consciously if it's low priority for your threat model. This is the highest-effort item and often the lowest payoff per hour spent.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Caveats worth knowing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We tested from a single European location. If you've got US or APAC traffic, a fully EU-only stack will add real latency you need to benchmark separately.&lt;/li&gt;
&lt;li&gt;Legal classification here is based on published transparency reports and EDPB guidance as of early 2025, not a review of your specific contracts. SCCs, the EU-US Data Privacy Framework, and things like AWS's "Digital Sovereignty Pledge" all interact with this picture in ways a pure infra test can't capture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Bottom line
&lt;/h3&gt;

&lt;p&gt;FISA 702 reauthorization doesn't change the underlying math, it's been in force since 2008. What's changed is that RFPs from regulated industries now explicitly ask vendors to document their exposure. If you haven't audited your sub-processor chain, that's the actual work here, not swapping your primary cloud region.&lt;/p&gt;

&lt;p&gt;Full methodology and extended results: &lt;a href="https://binadit.com/blog/measuring-fisa-702-reauthorization-infrastructure-management-services" rel="noopener noreferrer"&gt;Measuring what FISA 702 reauthorization actually changes for EU SaaS infrastructure&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/measuring-fisa-702-reauthorization-infrastructure-management-services" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How DNS resolution works under the hood: a step by step guide for high availability infrastructure</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 19 Aug 2026 07:10:37 +0000</pubDate>
      <link>https://dev.to/binadit/how-dns-resolution-works-under-the-hood-a-step-by-step-guide-for-high-availability-infrastructure-51mk</link>
      <guid>https://dev.to/binadit/how-dns-resolution-works-under-the-hood-a-step-by-step-guide-for-high-availability-infrastructure-51mk</guid>
      <description>&lt;h1&gt;
  
  
  DNS resolution: the seven hops nobody thinks about until failover breaks
&lt;/h1&gt;

&lt;p&gt;You push a config change, wait for propagation, and traffic still hits the dead server. Sound familiar? Most DNS incidents come down to not knowing what actually happens between a browser typing a domain and a TCP handshake starting. Let's fix that.&lt;/p&gt;

&lt;p&gt;This is a practical walkthrough of the resolution chain, plus the config patterns that turn DNS into an active part of your high availability setup instead of a silent single point of failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You control a domain through Cloudflare, Route 53, or similar, with dashboard or API access&lt;/li&gt;
&lt;li&gt;You have a Linux box with &lt;code&gt;dig&lt;/code&gt;, &lt;code&gt;nslookup&lt;/code&gt;, and &lt;code&gt;tcpdump&lt;/code&gt;/&lt;code&gt;ngrep&lt;/code&gt; available&lt;/li&gt;
&lt;li&gt;You know the basics of IP, TCP, and UDP&lt;/li&gt;
&lt;li&gt;You're comfortable running commands against a real or disposable test domain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll use &lt;code&gt;example.com&lt;/code&gt; everywhere below. Swap it for your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  The resolution chain, hop by hop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Browser cache
&lt;/h3&gt;

&lt;p&gt;Before anything hits the network, Chrome checks its own DNS cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chrome://net-internals/#dns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If there's a live entry, resolution stops right there. This is why a DNS change can look "stuck" in your own browser even after the TTL has expired everywhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OS stub resolver
&lt;/h3&gt;

&lt;p&gt;Next stop is the OS-level resolver, usually &lt;code&gt;systemd-resolved&lt;/code&gt; or &lt;code&gt;nscd&lt;/code&gt; on Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resolvectl status
resolvectl statistics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It checks &lt;code&gt;/etc/hosts&lt;/code&gt;, then its own cache, before forwarding anything upstream.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Recursive resolver
&lt;/h3&gt;

&lt;p&gt;No local hit means the query goes to a recursive resolver: your ISP's, or a public one like &lt;code&gt;1.1.1.1&lt;/code&gt; or &lt;code&gt;8.8.8.8&lt;/code&gt;. This is where &lt;code&gt;+trace&lt;/code&gt; becomes your best debugging friend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig @1.1.1.1 example.com +trace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Root servers
&lt;/h3&gt;

&lt;p&gt;The recursive resolver asks a root server who's authoritative for &lt;code&gt;.com&lt;/code&gt;. Roots don't know about &lt;code&gt;example.com&lt;/code&gt;, only about the TLD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. TLD servers
&lt;/h3&gt;

&lt;p&gt;The TLD server hands back your domain's authoritative nameservers, the ones set at your registrar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com. 172800 IN NS ns1.yourdnsprovider.com.
example.com. 172800 IN NS ns2.yourdnsprovider.com.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6. Authoritative nameserver
&lt;/h3&gt;

&lt;p&gt;Finally, the actual record comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com. 300 IN A 203.0.113.42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the step that matters for HA: which IP gets returned, how fast it can change, and what happens when that IP goes dark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring DNS for actual failover
&lt;/h2&gt;

&lt;p&gt;Understanding the chain is step one. Making it work for you is step two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Drop your TTLs on failover-critical records.&lt;/strong&gt; A 3600s TTL means an hour-long tail of stale traffic during failover. Use 60-300s instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com. 60 IN A 203.0.113.42
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll pay for it in query volume against your authoritative nameservers. Worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attach health checks.&lt;/strong&gt; Route 53 and Cloudflare can pull an unhealthy origin out of the response set automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws route53 change-resource-record-sets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--hosted-zone-id&lt;/span&gt; Z1PA6795UKMFR9 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--change-batch&lt;/span&gt; &lt;span class="s1"&gt;'{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "example.com",
        "Type": "A",
        "SetIdentifier": "primary",
        "Failover": "PRIMARY",
        "TTL": 60,
        "ResourceRecords": [{"Value": "203.0.113.42"}],
        "HealthCheckId": "abcd1234-healthcheck-id"
      }
    }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same mechanism behind a clean zero-downtime migration: DNS shifts traffic instead of forcing a hard cutover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Return multiple A records.&lt;/strong&gt; Clients can fall back to a second IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;example.com. 300 IN A 203.0.113.42
example.com. 300 IN A 203.0.113.43
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine for stateless services behind a load balancer, but not a real substitute for health-checked failover; clients cache order and don't always retry smartly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying it actually works
&lt;/h2&gt;

&lt;p&gt;Don't trust the dashboard. Check every layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig example.com +noall +answer
dig example.com @8.8.8.8 +noall +answer
dig example.com @1.1.1.1 +noall +answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lower-than-configured TTLs from a resolver mean the change is propagating. Original TTLs mean you're still hitting a cached answer.&lt;/p&gt;

&lt;p&gt;Check resolution latency directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dig example.com | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"Query time"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consistently above 100-150ms? Look at resolver placement or a provider with more edge presence. This happens before the TCP handshake even starts, so it's pure overhead on TTFB.&lt;/p&gt;

&lt;p&gt;Simulate failover instead of waiting for an incident to test it for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; INPUT &lt;span class="nt"&gt;-s&lt;/span&gt; &amp;lt;health-check-ip&amp;gt; &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
watch &lt;span class="nt"&gt;-n&lt;/span&gt; 5 &lt;span class="s1"&gt;'dig example.com +short'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the response flips to your secondary within the configured TTL, then remove the rule.&lt;/p&gt;

&lt;p&gt;Add DNS resolution time to your monitoring as its own metric, separate from full page load. A spike there with stable backend times points straight at a resolver or nameserver problem, not your app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls that keep coming back
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default TTLs.&lt;/strong&gt; 3600 or 86400 seconds quietly kills any failover plan built on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single nameserver provider.&lt;/strong&gt; One outage there takes your domain down regardless of server health.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNSSEC misconfiguration.&lt;/strong&gt; Broken signing chains fail silently for validating resolvers while looking fine in tools that skip validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing only from your machine.&lt;/strong&gt; Your local cache hides what real users see. Always check against multiple public resolvers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep CNAME chains.&lt;/strong&gt; Each extra hop adds latency and another point of failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understand the seven hops, and DNS stops being a mystery box that occasionally ruins your day.&lt;/p&gt;

&lt;p&gt;Full original writeup: &lt;a href="https://binadit.com/blog/how-dns-resolution-works-high-availability-infrastructure" rel="noopener noreferrer"&gt;How DNS resolution works under the hood&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/how-dns-resolution-works-high-availability-infrastructure" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>TTFB vs full page load: choosing the right metric for your ecommerce infrastructure</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:41:20 +0000</pubDate>
      <link>https://dev.to/binadit/ttfb-vs-full-page-load-choosing-the-right-metric-for-your-ecommerce-infrastructure-2e57</link>
      <guid>https://dev.to/binadit/ttfb-vs-full-page-load-choosing-the-right-metric-for-your-ecommerce-infrastructure-2e57</guid>
      <description>&lt;h1&gt;
  
  
  Stop chasing TTFB: it's not the metric that's tanking your conversions
&lt;/h1&gt;

&lt;p&gt;Your TTFB dashboard is green. Your conversion rate is red. If that combination sounds familiar, you're not alone, and you're not chasing the wrong fix by accident, you're chasing it because every tool you use puts TTFB front and center.&lt;/p&gt;

&lt;p&gt;Here's the problem: TTFB and full page load metrics answer completely different questions, and treating them as interchangeable wastes engineering time on ecommerce sites where every millisecond on checkout pages has a dollar value attached.&lt;/p&gt;

&lt;h2&gt;
  
  
  What TTFB actually tells you
&lt;/h2&gt;

&lt;p&gt;TTFB is the clock between the request leaving the browser and the first byte coming back. That window includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DNS resolution (if uncached)&lt;/li&gt;
&lt;li&gt;TCP/TLS handshake&lt;/li&gt;
&lt;li&gt;Server-side work: routing, DB queries, template rendering, cache lookups&lt;/li&gt;
&lt;li&gt;Network transit for that first byte&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a warm HTTP/2 or HTTP/3 connection, server processing time dominates this number. Which makes TTFB genuinely useful, but only for one job: telling you when your backend is under load.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TTFB baseline: 180ms
TTFB during flash sale: 900ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That jump isn't noise. It's your app server, DB, or cache layer running out of headroom before a single byte of HTML goes out. On WooCommerce stacks especially, a TTFB spike during a traffic surge is usually the first hard evidence that your PHP-FPM pool or database is maxed out, well before cart abandonment shows up in analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where TTFB earns its keep
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Catching server-side regressions.&lt;/strong&gt; A slow query, an N+1 bug, a cold cache: TTFB flags these before users notice anything visually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Load testing and capacity planning.&lt;/strong&gt; Watch TTFB climb under simulated load and you know exactly where your app tier breaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Splitting backend vs frontend blame.&lt;/strong&gt; Fast TTFB + slow-feeling page = stop looking at the server, start looking at JS and render-blocking assets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where it lies to you
&lt;/h3&gt;

&lt;p&gt;A 100ms TTFB tells you nothing about what happens after that byte lands. The page can still take 6 seconds to become usable because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render-blocking CSS/JS&lt;/li&gt;
&lt;li&gt;Images without dimensions causing layout shift&lt;/li&gt;
&lt;li&gt;A tag manager script hogging the main thread&lt;/li&gt;
&lt;li&gt;Late-loading fonts (hello, flash of invisible text)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And here's the trap most teams miss: if a response is served from a CDN edge cache, TTFB measures the edge, not your origin. Your dashboard can look perfectly healthy while your actual application is quietly degrading, right up until a cache miss exposes it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  What full page load metrics tell you instead
&lt;/h2&gt;

&lt;p&gt;LCP, Speed Index, fully-loaded time: these track what the user actually sees and feels. That's why Google leans on LCP and CLS for Core Web Vitals; TTFB alone was a bad predictor of both perceived speed and search ranking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strengths:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correlates with real user experience, not just server response&lt;/li&gt;
&lt;li&gt;Surfaces conversion killers directly (a bloated hero image, a blocking checkout script)&lt;/li&gt;
&lt;li&gt;Feeds into Core Web Vitals and SEO scoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Weaknesses:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Noisy. A regressed LCP could be caused by the backend, a new marketing pixel, an unoptimized CMS upload, or a hydration delay in your frontend framework&lt;/li&gt;
&lt;li&gt;Lagging indicator. You need waterfall analysis and resource timing to find the actual cause after LCP tanks in prod&lt;/li&gt;
&lt;li&gt;Out of infra's hands. You can run flawless high-availability infrastructure and still ship a slow LCP because a third-party payment widget is garbage&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The decision framework
&lt;/h2&gt;

&lt;p&gt;Stop asking "is TTFB good." Ask this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TTFB_high&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;LCP_high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;fixBackendFirst&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// nothing downstream matters until this is solved&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TTFB_low&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;LCP_high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;profileBrowserWaterfall&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// frontend/third-party problem&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TTFB_high&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;LCP_low&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// rare, but check if LCP measurement is misleading you&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;servingMostlyFromCDN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;trackOriginResponseTimeSeparately&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// edge TTFB will lie to you&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical ownership split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Metric to watch&lt;/th&gt;
&lt;th&gt;Who owns it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Debugging capacity/backend regression&lt;/td&gt;
&lt;td&gt;TTFB&lt;/td&gt;
&lt;td&gt;Backend/infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debugging conversion drop or CWV score drop&lt;/td&gt;
&lt;td&gt;LCP, Speed Index&lt;/td&gt;
&lt;td&gt;Frontend + infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prepping for a sale or launch&lt;/td&gt;
&lt;td&gt;Both, together&lt;/td&gt;
&lt;td&gt;Whole team&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For traffic events specifically: TTFB tells you when the servers start straining, LCP tells you whether shoppers are actually feeling it. Alert on both, but don't confuse one for the other, and don't let a green TTFB dashboard talk you out of investigating a real conversion problem.&lt;/p&gt;

&lt;p&gt;Full writeup with the complete comparison table and more edge cases: &lt;a href="https://binadit.com/blog/ttfb-vs-full-page-load-metric-ecommerce-infrastructure" rel="noopener noreferrer"&gt;TTFB vs full page load: choosing the right metric for your ecommerce infrastructure&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/ttfb-vs-full-page-load-metric-ecommerce-infrastructure" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Measuring how EuroStack changes the procurement conversation for infrastructure management services</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 12 Aug 2026 07:12:58 +0000</pubDate>
      <link>https://dev.to/binadit/measuring-how-eurostack-changes-the-procurement-conversation-for-infrastructure-management-services-43md</link>
      <guid>https://dev.to/binadit/measuring-how-eurostack-changes-the-procurement-conversation-for-infrastructure-management-services-43md</guid>
      <description>&lt;h1&gt;
  
  
  We ran the numbers on 41 procurement deals to see if EuroStack is real or just RFP theater
&lt;/h1&gt;

&lt;p&gt;If you've touched infrastructure procurement in the last year, you've probably seen "EU sovereignty" or "EuroStack" show up somewhere in the RFP. The question every engineering lead asks: is this actual signal that changes vendor selection, or is it a compliance checkbox that nobody enforces?&lt;/p&gt;

&lt;p&gt;We had visibility into 41 procurement processes between January 2023 and September 2025, so instead of guessing, we split the data before and after EuroStack-style criteria showed up explicitly in tender docs and measured what actually shifted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;We defined "EuroStack criteria" narrowly. It had to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EU legal jurisdiction over the operating entity&lt;/li&gt;
&lt;li&gt;Data processing location inside the EU/EEA&lt;/li&gt;
&lt;li&gt;Disclosure of subcontractor data flows, including support and monitoring tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;"GDPR compliant" with no operational detail didn't count. That's boilerplate, not a filter.&lt;/p&gt;

&lt;p&gt;Sample: 41 contracts, €40k to €2.1M ACV, across SaaS (14), public sector (11), fintech/payments (9), and e-commerce/logistics (7). 19 processes ran before explicit sovereignty language appeared, 22 ran after.&lt;/p&gt;

&lt;p&gt;We were not benchmarking server performance here. This is purely procurement mechanics: shortlist size, timelines, contract clauses, final price vs. quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually moved
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Metric                              Before    After
Avg. vendors shortlisted            5.2       3.6
% non-EU hyperscaler-reseller-only  47%       12%
Avg. RFP-to-signature time          11.4wk    7.9wk
% with subcontractor disclosure     21%       86%
% with real data residency SLA      32%       79%
Avg. final value vs. initial quote  +14%      +4%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timeline tail matters more than the average:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Percentile   Before (wk)   After (wk)
p50          9.5           6.0
p95          24.0          15.5
p99          31.0          19.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-signature scope disputes also dropped hard: 42% of "before" contracts had a change request or renegotiation in the first 6 months, usually over data location or support access. In the "after" group, that fell to 18%.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;The shortlist shrinking from 5.2 to 3.6 vendors isn't a loss, it's a filter working correctly. Reseller setups that white-label a US hyperscaler and can't answer "where does support tooling send logs" get cut early instead of surviving three rounds of vague reassurance.&lt;/p&gt;

&lt;p&gt;That filtering effect is also why timelines compressed. When the RFP already specifies data residency and subcontractor disclosure, vendors don't need three rounds of clarifying questions. We saw this in our own bid pipeline: RFP-to-proposal time dropped from 9 days to 4 when the tender pre-answered the questions we'd otherwise have to ask ourselves.&lt;/p&gt;

&lt;p&gt;The contract value delta is the part people underestimate. Vague sovereignty language at RFP stage doesn't disappear, it just resurfaces post-signature as a change order once someone discovers a gap. Explicit criteria up front push that negotiation into the cheap phase instead of the expensive one.&lt;/p&gt;

&lt;p&gt;One more pattern worth flagging: buyers who specified sovereignty criteria were also far more likely to require named engineer contacts instead of ticket-based support (61% vs. 23%). Teams that care about knowing where their data lives also tend to care about knowing who picks up the phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats before you cite this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Selection bias&lt;/strong&gt;: we bid on EU-sovereign contracts, so our "before" sample skews toward processes where we were still invited. Fully non-sovereignty procurement is likely faster and larger in scope than what we captured.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small sample&lt;/strong&gt;: 41 deals over 2.5 years shows a trend, not statistical proof. One outlier public-sector tender inflated the before-p99 from 24 to 31 weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No official standard exists yet&lt;/strong&gt;: "EuroStack criteria" is our own classification. There's no certification scheme to point to as of late 2025.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cost-per-compute comparison&lt;/strong&gt;: this says nothing about whether EU infra is cheaper or pricier per server-hour than hyperscaler equivalents. Different question, different analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway for anyone writing or responding to RFPs
&lt;/h2&gt;

&lt;p&gt;If you're a buyer: specific sovereignty requirements (jurisdiction, data location, subcontractor disclosure) act as a fast, cheap filter that eliminates non-answers early and shrinks your timeline. If you're a vendor: if you can't answer where support tooling sends logs, you're going to keep losing time to vendors who can.&lt;/p&gt;

&lt;p&gt;Full methodology and data breakdown in the &lt;a href="https://binadit.com/blog/measuring-eurostack-infrastructure-management-services-procurement" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/measuring-eurostack-infrastructure-management-services-procurement" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>10 practices that keep WooCommerce fast at scale</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 07 Aug 2026 07:11:37 +0000</pubDate>
      <link>https://dev.to/binadit/10-practices-that-keep-woocommerce-fast-at-scale-3oje</link>
      <guid>https://dev.to/binadit/10-practices-that-keep-woocommerce-fast-at-scale-3oje</guid>
      <description>&lt;h2&gt;
  
  
  Your WooCommerce store is fine until it isn't
&lt;/h2&gt;

&lt;p&gt;Everything runs smoothly until a flash sale hits, or your ERP sync job starts locking tables during checkout hours. If you're running WooCommerce past a few hundred SKUs or a few thousand orders a month, you've probably already hit one of these walls. Good news: none of this requires a replatform. It's config, query discipline, and infrastructure decisions you can roll out incrementally.&lt;/p&gt;

&lt;p&gt;Here are 10 practices that actually move the needle, ranked roughly by how fast you'll feel the impact.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Get sessions and cart data out of MySQL
&lt;/h3&gt;

&lt;p&gt;By default, WooCommerce dumps cart and session data into &lt;code&gt;wp_options&lt;/code&gt; and &lt;code&gt;wp_woocommerce_sessions&lt;/code&gt;. Under concurrent load, that's write contention on tables that are already getting hammered by product and pricing reads.&lt;/p&gt;

&lt;p&gt;Move it to Redis. Sub-millisecond lookups instead of a MySQL round trip per cart update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// wp-config.php or a custom session handler plugin&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_REDIS_HOST'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'127.0.0.1'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_REDIS_PORT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_REDIS_TIMEOUT'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nb"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'WP_REDIS_DATABASE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Don't conflate object cache with page cache
&lt;/h3&gt;

&lt;p&gt;Object caching (Redis/Memcached) caches individual query results; product lookups, term queries, user meta. Page caching serves fully rendered HTML. They need different invalidation logic entirely.&lt;/p&gt;

&lt;p&gt;Cart and category pages aren't fully cacheable at the page level since they're price-sensitive and user-specific, but they still benefit massively from object caching underneath. Keep them separated and you get both speed and correctness.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Never let full-page cache touch cart, checkout, or account pages
&lt;/h3&gt;

&lt;p&gt;A cache plugin that doesn't understand WooCommerce's dynamic fragments will happily serve one customer's cart to another. That's not a performance win, that's a bug report waiting to happen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nginx: never cache dynamic WooCommerce endpoints&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;^/(cart|checkout|my-account)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_no_cache&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else (product pages, categories, homepage) can be cached aggressively as long as cart fragments load via AJAX post-render.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Stop refreshing cart fragments on pages where nothing changed
&lt;/h3&gt;

&lt;p&gt;The default mini-cart AJAX call (&lt;code&gt;wc-ajax=get_refreshed_fragments&lt;/code&gt;) is what makes point 3 possible, but it's commonly left running on every page load, blog posts included. Throttle it or disable it on pages where cart state can't change. Noticeable PHP-FPM load reduction on content-heavy stores.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Audit your indexes, not just WooCommerce's defaults
&lt;/h3&gt;

&lt;p&gt;Default indexes on &lt;code&gt;wp_postmeta&lt;/code&gt; and &lt;code&gt;wp_wc_order_stats&lt;/code&gt; are fine for standard queries. Custom filters, ERP syncs, and reporting plugins introduce meta queries that often aren't indexed at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Check for missing composite indexes&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;post_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_postmeta&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;meta_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'_stock_status'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;meta_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'instock'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- If it's a full table scan:&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;wp_postmeta&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_meta_key_value&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;meta_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;meta_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do this quarterly. Plugin updates change query patterns, and indexes don't fix themselves.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Queue everything that isn't the checkout itself
&lt;/h3&gt;

&lt;p&gt;Order confirmation emails, ERP syncs, webhooks: none of that should run synchronously inside the checkout request. If a downstream system is slow, your customer is the one waiting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Defer post-order work instead of running it inline&lt;/span&gt;
&lt;span class="nf"&gt;add_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'woocommerce_order_status_completed'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;as_schedule_single_action&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s1"&gt;'sync_order_to_erp'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'order_id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$order_id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Action Scheduler ships with WooCommerce already; use it, or push to a Redis-backed queue. Keep the request path short.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. CDN your images, not just your HTML
&lt;/h3&gt;

&lt;p&gt;Product catalogs with hundreds of high-res images are usually the biggest bandwidth drain on the whole stack.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;.(jpg|jpeg|png|webp|avif)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;expires&lt;/span&gt; &lt;span class="s"&gt;30d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Cache-Control&lt;/span&gt; &lt;span class="s"&gt;"public,&lt;/span&gt; &lt;span class="s"&gt;immutable"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Serve WebP/AVIF where supported. This has a direct effect on mobile checkout conversion, arguably more than most backend tuning.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Autoscale PHP-FPM based on concurrency, not vibes
&lt;/h3&gt;

&lt;p&gt;A fixed &lt;code&gt;pm.max_children&lt;/code&gt; tuned for average traffic will choke during a flash sale and sit idle the rest of the time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;pm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;dynamic&lt;/span&gt;
&lt;span class="py"&gt;pm.max_children&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;
&lt;span class="py"&gt;pm.start_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="py"&gt;pm.min_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5&lt;/span&gt;
&lt;span class="py"&gt;pm.max_spare_servers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;20&lt;/span&gt;
&lt;span class="py"&gt;pm.max_requests&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pm.max_requests&lt;/code&gt; is underrated: it forces worker recycling and prevents slow memory leaks in plugin code from degrading performance over a day.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Send reporting and sync jobs to a read replica
&lt;/h3&gt;

&lt;p&gt;Analytics dashboards and ERP syncs run expensive aggregate queries. Running them against your primary competes directly with checkout for connections and locks. Point them at a replica instead and isolate the load completely.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Load test the checkout flow, not the homepage
&lt;/h3&gt;

&lt;p&gt;Homepage numbers look great because it's the most cacheable page you have. Checkout is the opposite: least cacheable, most DB-intensive, and the one that actually determines revenue. Simulate the real flow: add to cart, apply coupon, enter payment, submit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollout order that won't blow up your team
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 1-2&lt;/strong&gt;: object cache + session offload (1-2). Zero code risk, immediate latency win.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weeks 3-4&lt;/strong&gt;: cache exclusions + CDN config (3, 7). Low risk, big TTFB improvement for anonymous traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month 2&lt;/strong&gt;: query/index audit + queue migration (5, 6). Touches business logic, test accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Month 2-3&lt;/strong&gt;: PHP-FPM tuning + read replica (8, 9). Do these once you have real traffic data to tune against.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read the full original writeup here: &lt;a href="https://binadit.com/blog/woocommerce-fast-at-scale-managed-infrastructure-for-saas" rel="noopener noreferrer"&gt;woocommerce-fast-at-scale-managed-infrastructure-for-saas&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/woocommerce-fast-at-scale-managed-infrastructure-for-saas" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Setting up sovereign cloud reference architectures: three patterns that work</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 05 Aug 2026 07:11:52 +0000</pubDate>
      <link>https://dev.to/binadit/setting-up-sovereign-cloud-reference-architectures-three-patterns-that-work-4me1</link>
      <guid>https://dev.to/binadit/setting-up-sovereign-cloud-reference-architectures-three-patterns-that-work-4me1</guid>
      <description>&lt;h1&gt;
  
  
  Three sovereign cloud patterns that actually hold up under audit
&lt;/h1&gt;

&lt;p&gt;Here's the problem: picking "EU" from a cloud console dropdown is not sovereignty. It's a checkbox. Real data residency means your compute, your backups, your third-party services, and your failover path all stay inside a legal boundary, and you can prove it with a packet capture, not just a policy doc.&lt;/p&gt;

&lt;p&gt;We've deployed three reference patterns repeatedly for SaaS platforms, agencies, and e-commerce clients dealing with GDPR, DORA, or public-sector requirements. Here's how each one works, with the configs we actually run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before you start
&lt;/h2&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stateless app tier (no session affinity dependencies)&lt;/li&gt;
&lt;li&gt;Terraform/OpenTofu plus Ansible or equivalent&lt;/li&gt;
&lt;li&gt;A clear map of which data is regulated and which isn't&lt;/li&gt;
&lt;li&gt;Access to at least one EU-based provider, plus a public cloud account if you're going hybrid&lt;/li&gt;
&lt;li&gt;Comfort with HAProxy/Nginx and PostgreSQL/MySQL replication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you haven't decided between open-source infra and managed public cloud yet, sort that out first. Everything below assumes it's settled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 1: Single-region EU
&lt;/h2&gt;

&lt;p&gt;The baseline. Good for most SaaS teams that need provable EU residency without multi-region complexity.&lt;/p&gt;

&lt;p&gt;Everything (compute, storage, backups, logging, error tracking) stays in one EU region. Spread compute across at least two AZs, never a single rack.&lt;/p&gt;

&lt;p&gt;Load balancer tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;frontend&lt;/span&gt; &lt;span class="n"&gt;web_front&lt;/span&gt;
    &lt;span class="n"&gt;bind&lt;/span&gt; *:&lt;span class="m"&gt;443&lt;/span&gt; &lt;span class="n"&gt;ssl&lt;/span&gt; &lt;span class="n"&gt;crt&lt;/span&gt; /&lt;span class="n"&gt;etc&lt;/span&gt;/&lt;span class="n"&gt;haproxy&lt;/span&gt;/&lt;span class="n"&gt;certs&lt;/span&gt;/&lt;span class="n"&gt;app&lt;/span&gt;.&lt;span class="n"&gt;pem&lt;/span&gt;
    &lt;span class="n"&gt;mode&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;
    &lt;span class="n"&gt;default_backend&lt;/span&gt; &lt;span class="n"&gt;app_servers&lt;/span&gt;

&lt;span class="n"&gt;backend&lt;/span&gt; &lt;span class="n"&gt;app_servers&lt;/span&gt;
    &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="n"&gt;roundrobin&lt;/span&gt;
    &lt;span class="n"&gt;option&lt;/span&gt; &lt;span class="n"&gt;httpchk&lt;/span&gt; &lt;span class="n"&gt;GET&lt;/span&gt; /&lt;span class="n"&gt;health&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;app1&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;11&lt;/span&gt;:&lt;span class="m"&gt;8080&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;app2&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;12&lt;/span&gt;:&lt;span class="m"&gt;8080&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="n"&gt;app3&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;.&lt;span class="m"&gt;0&lt;/span&gt;.&lt;span class="m"&gt;1&lt;/span&gt;.&lt;span class="m"&gt;13&lt;/span&gt;:&lt;span class="m"&gt;8080&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL synchronous replication for zero data loss on failover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# postgresql.conf on primary
&lt;/span&gt;&lt;span class="n"&gt;synchronous_standby_names&lt;/span&gt; = &lt;span class="s1"&gt;'standby1'&lt;/span&gt;
&lt;span class="n"&gt;wal_level&lt;/span&gt; = &lt;span class="n"&gt;replica&lt;/span&gt;
&lt;span class="n"&gt;max_wal_senders&lt;/span&gt; = &lt;span class="m"&gt;5&lt;/span&gt;

&lt;span class="c"&gt;# recovery config on standby
&lt;/span&gt;&lt;span class="n"&gt;primary_conninfo&lt;/span&gt; = &lt;span class="s1"&gt;'host=10.0.1.21 port=5432 user=replicator'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part everyone forgets: route email, error tracking, and analytics through EU-based providers too. A perfectly compliant database means nothing if your error tracker ships stack traces to a US endpoint.&lt;/p&gt;

&lt;p&gt;Back up daily, encrypt everything, keep it in-region. 30 days daily / 12 months monthly is a reasonable default for compliance workloads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 2: Active-passive multi-region
&lt;/h2&gt;

&lt;p&gt;For disaster recovery across two EU regions (say, Amsterdam and Frankfurt) without paying for a full duplicate stack.&lt;/p&gt;

&lt;p&gt;Size the passive region at 30-50% capacity, enough to run degraded, not enough to bleed money idling.&lt;/p&gt;

&lt;p&gt;Async replication to the secondary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="c"&gt;# On secondary region standby
&lt;/span&gt;&lt;span class="n"&gt;primary_conninfo&lt;/span&gt; = &lt;span class="s1"&gt;'host= port=5432 user=replicator sslmode=require'&lt;/span&gt;
&lt;span class="n"&gt;restore_command&lt;/span&gt; = &lt;span class="s1"&gt;'cp /archive/%f %p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DNS-based failover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;failover_policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-west (Amsterdam)&lt;/span&gt;
  &lt;span class="na"&gt;secondary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eu-central (Frankfurt)&lt;/span&gt;
  &lt;span class="na"&gt;health_check_interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
  &lt;span class="na"&gt;failover_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3 consecutive failures&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate the promotion, don't SSH in under pressure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pg_ctl promote &lt;span class="nt"&gt;-D&lt;/span&gt; /var/lib/postgresql/data
&lt;span class="c"&gt;# update connection strings via config management, never manual edits&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And actually test failover quarterly. This is the step everyone skips, and it's the only one that proves the whole thing works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pattern 3: Hybrid private-public
&lt;/h2&gt;

&lt;p&gt;Best for regulated workloads (payments, health data, government contracts) that also need elastic burst capacity for traffic spikes.&lt;/p&gt;

&lt;p&gt;Keep the system of record (database, PII, payment processing) on private EU infrastructure. Push only stateless, non-sensitive work (static assets, image processing, caching) to public cloud.&lt;/p&gt;

&lt;p&gt;Enforce the boundary at the network layer, not just in app code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Restrict outbound DB traffic to private subnet only&lt;/span&gt;
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 5432 &lt;span class="nt"&gt;-d&lt;/span&gt; 10.0.0.0/8 &lt;span class="nt"&gt;-j&lt;/span&gt; ACCEPT
iptables &lt;span class="nt"&gt;-A&lt;/span&gt; OUTPUT &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 5432 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Route burst traffic through a CDN that actually keeps EU traffic on EU edge nodes (not all of them do, check before you commit). Keep a data flow diagram ready; procurement and auditors will ask for it before signing anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it, don't just deploy it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Residency&lt;/strong&gt;: traceroute and packet capture a real transaction, confirm no hop leaves the EU&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failover time&lt;/strong&gt;: target under 60 seconds with a 10-second health check interval; over 5 minutes means your thresholds are too conservative&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replication lag&lt;/strong&gt;: &lt;code&gt;SELECT now() - pg_last_xact_replay_timestamp();&lt;/code&gt; on the standby, sustained lag over 5s means trouble on unplanned failover&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boundary enforcement&lt;/strong&gt;: try connecting from the public segment to the private DB port, confirm the firewall (not app logic) rejects it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real downtime&lt;/strong&gt;: run a drill and measure customer-facing downtime, not just the infra switch; DNS caching often adds 2-5 minutes you didn't account for&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls worth repeating
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Selecting "EU" in a console is not proof of anything, verify subprocessors and backup locations contractually and technically&lt;/li&gt;
&lt;li&gt;Third-party services are the most commonly skipped audit item&lt;/li&gt;
&lt;li&gt;Under-sizing the passive region means it won't actually hold real traffic when it matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full details and more context in the &lt;a href="https://binadit.com/blog/sovereign-cloud-reference-architectures-managed-cloud-infrastructure-patterns" rel="noopener noreferrer"&gt;original article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/sovereign-cloud-reference-architectures-managed-cloud-infrastructure-patterns" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cloud</category>
      <category>infrastructure</category>
      <category>terraform</category>
    </item>
    <item>
      <title>How zero-downtime database migrations work</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 31 Jul 2026 07:28:52 +0000</pubDate>
      <link>https://dev.to/binadit/how-zero-downtime-database-migrations-work-4k1e</link>
      <guid>https://dev.to/binadit/how-zero-downtime-database-migrations-work-4k1e</guid>
      <description>&lt;h1&gt;
  
  
  Zero-downtime database migrations: a practical breakdown
&lt;/h1&gt;

&lt;p&gt;You ship a schema change on a Friday afternoon. Thirty seconds later, error rates spike across every pod running the old deploy. Nothing was technically wrong with your SQL; the problem was timing, not syntax. This is the scenario zero-downtime migration practices exist to prevent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real problem isn't the migration
&lt;/h2&gt;

&lt;p&gt;Zero-downtime doesn't mean the migration completes instantly. It means your app keeps serving correct reads and writes while the schema shifts from old to new. Most migration failures trace back to one of three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lock contention on the table you're altering&lt;/li&gt;
&lt;li&gt;Application code assuming a schema state that no longer exists&lt;/li&gt;
&lt;li&gt;A cutover step that briefly knocks the primary offline&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix starts with a mental shift: treat migrations as a sequence of small, reversible steps, not one big-bang deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's happening under the hood
&lt;/h2&gt;

&lt;p&gt;Your app servers carry a schema assumption baked into ORM models and prepared statements. The database holds the actual schema. During a migration, you're moving both in sync, and there has to be no moment where they disagree in a way that breaks a query.&lt;/p&gt;

&lt;p&gt;Schema changes generally fall into three buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Additive&lt;/strong&gt; (new column, table, index): usually safe, but can still lock depending on the engine and default value behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Destructive&lt;/strong&gt; (dropping a column, renaming a table): dangerous if any live code still references the old structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transformative&lt;/strong&gt; (changing a column type, splitting a table): requires both old and new structures to coexist temporarily&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pattern: expand-contract
&lt;/h2&gt;

&lt;p&gt;Also called parallel change. Four phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Expand&lt;/strong&gt;: add the new element alongside the old one. Nothing removed yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migrate&lt;/strong&gt;: backfill data into the new structure, dual-write from the app, or read-with-fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt;: confirm consistency under real production traffic, not staging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract&lt;/strong&gt;: once every app instance is confirmed on the new structure, drop the old one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works because at every step, both the old and new app deploy can run against the schema without erroring. That matters because deploys are rolling, not instant; you might have two code versions hitting the same DB for 30-90 seconds. If your migration doesn't tolerate that overlap, you'll see error spikes that look random but are completely deterministic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Locking details worth knowing
&lt;/h3&gt;

&lt;p&gt;In PostgreSQL (11+), adding a column with a constant default is metadata-only; milliseconds regardless of table size. Adding a column with a non-constant default, or a NOT NULL constraint without a default, can force a full table rewrite under an ACCESS EXCLUSIVE lock, blocking everything.&lt;/p&gt;

&lt;p&gt;MySQL's InnoDB has a similar split: some ALTER TABLE operations use INSTANT or INPLACE algorithms with no locking; others fall back to COPY, rebuilding the whole table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walking through a real example
&lt;/h2&gt;

&lt;p&gt;Say you're renaming &lt;code&gt;status&lt;/code&gt; to &lt;code&gt;order_status&lt;/code&gt; on a 40 million row &lt;code&gt;orders&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What breaks:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;RENAME&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;order_status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fast in Postgres, metadata-only. But any app server still on the previous deploy referencing &lt;code&gt;status&lt;/code&gt; starts throwing "column does not exist" the instant this commits. Across 12 pods mid-rollout, that's a guaranteed multi-minute error spike.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expand-contract version:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Step 1: expand&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;order_status&lt;/span&gt; &lt;span class="nb"&gt;varchar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 2: backfill in batches, avoid long-running transactions&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;order_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="mi"&gt;50000&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;order_status&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- repeat in batches of 50k&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 3: dual-write in app code so new rows populate both columns&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 4: deploy code that reads order_status, falls back to status,&lt;/span&gt;
&lt;span class="c1"&gt;-- for one full release cycle&lt;/span&gt;

&lt;span class="c1"&gt;-- Step 5: after zero fallback reads in logs for 7 days&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Batch size matters at this scale. One giant UPDATE across 40M rows generates enough WAL/binlog volume to cause replication lag, which cascades into stale reads on any replica-dependent service. Batches of 10k-50k with short pauses keep lag under a second and avoid blocking autovacuum.&lt;/p&gt;

&lt;p&gt;For bigger transformative changes (splitting a &lt;code&gt;users&lt;/code&gt; table into &lt;code&gt;users&lt;/code&gt; and &lt;code&gt;user_profiles&lt;/code&gt; at 200M rows), expect this rough timeline:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Duration&lt;/th&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expand&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backfill&lt;/td&gt;
&lt;td&gt;4-10 hours&lt;/td&gt;
&lt;td&gt;Low, if throttled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dual-write period&lt;/td&gt;
&lt;td&gt;1-2 weeks&lt;/td&gt;
&lt;td&gt;Medium, needs monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read cutover + verify&lt;/td&gt;
&lt;td&gt;3-5 days&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Contract&lt;/td&gt;
&lt;td&gt;Minutes&lt;/td&gt;
&lt;td&gt;Low, if verification passed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More elapsed time, dramatically smaller blast radius. That's the trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dual-write complexity&lt;/strong&gt;: two write paths double your bug surface. A missed dual-write is the most common cause of silent data drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Longer total timeline&lt;/strong&gt;: a 10-minute blocking migration can become a multi-week incremental one. For low-traffic internal tools, this often isn't worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replica lag&lt;/strong&gt;: even throttled backfills add write volume that can measurably increase lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback complexity&lt;/strong&gt;: a single ALTER TABLE rolls back trivially. A multi-phase migration needs a rollback plan per phase.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Tooling notes
&lt;/h2&gt;

&lt;p&gt;Online schema change tools like &lt;code&gt;gh-ost&lt;/code&gt; or &lt;code&gt;pt-online-schema-change&lt;/code&gt; for MySQL automate much of this by building a shadow table, replaying changes via triggers/binlog, and swapping atomically at the end. They save manual effort but add operational overhead: monitoring the copy, watching for lag, handling mid-copy failures on huge tables.&lt;/p&gt;

&lt;p&gt;For PostgreSQL, &lt;code&gt;CREATE INDEX CONCURRENTLY&lt;/code&gt; and instant column additions have reduced the need for third-party tools in many cases. Large rewrites (changing a column type) still often need manual expand-contract or extension support.&lt;/p&gt;

&lt;p&gt;Read the full breakdown here: &lt;a href="https://binadit.com/blog/zero-downtime-database-migration-infrastructure-performance-optimization" rel="noopener noreferrer"&gt;Zero-downtime database migrations&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/zero-downtime-database-migration-infrastructure-performance-optimization" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Choosing between the open-source sovereign stack and managed public cloud</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 29 Jul 2026 07:20:33 +0000</pubDate>
      <link>https://dev.to/binadit/choosing-between-the-open-source-sovereign-stack-and-managed-public-cloud-2jp</link>
      <guid>https://dev.to/binadit/choosing-between-the-open-source-sovereign-stack-and-managed-public-cloud-2jp</guid>
      <description>&lt;h1&gt;
  
  
  Sovereign stack or managed cloud: the real math behind the decision
&lt;/h1&gt;

&lt;p&gt;Your cloud bill just crossed six figures a month and someone on the platform team asked, "could we just... run this ourselves?" If you've had that conversation, you already know the answer isn't obvious. Let's break down when self-hosting Proxmox, Ceph, OpenStack, and Kubernetes actually beats renting from a hyperscaler, and when it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who actually hits this decision
&lt;/h2&gt;

&lt;p&gt;Three groups keep running into this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SaaS teams where cloud spend stopped scaling sanely&lt;/li&gt;
&lt;li&gt;Regulated companies (fintech, healthcare, gov contractors) that need provable EU data residency&lt;/li&gt;
&lt;li&gt;Platform teams burned by a surprise pricing change or an outage nobody on their side could fix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't really "open source vs cloud." It's "who operates your infrastructure": you, or a partner/hyperscaler doing it for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The sovereign stack, layer by layer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Kubernetes        &amp;lt;- container scheduling
   |
OpenStack         &amp;lt;- API layer: compute, Neutron networking, Keystone identity
   |
Ceph              &amp;lt;- distributed block/object/file storage
   |
Proxmox VE (KVM/LXC) &amp;lt;- virtualization layer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Proxmox VE&lt;/strong&gt;: your VMware replacement. KVM + LXC, web UI, clustering, built-in backup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ceph&lt;/strong&gt;: distributed storage with no single point of failure. Lose a node, keep serving traffic, &lt;em&gt;if&lt;/em&gt; your CRUSH map and replication factors are sane.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenStack&lt;/strong&gt;: turns a rack of Proxmox/KVM hosts into something you provision like EC2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes&lt;/strong&gt;: usually sits on top, scheduling containers on the VMs OpenStack provisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Real result: a fintech client cut cloud spend 65% moving core services to Proxmox + Ceph, keeping only CDN/edge on public cloud. Gov and defense-adjacent contractors like this stack because OpenStack's API compatibility means AWS-style automation, but on hardware sitting in an EU data center with zero CLOUD Act exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're actually signing up for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No per-core licensing tax (Proxmox support subscription, Ceph/OpenStack are Apache-licensed)&lt;/li&gt;
&lt;li&gt;Full control over physical location, network path, access&lt;/li&gt;
&lt;li&gt;Cost flattens hard past ~15-20 sustained VMs&lt;/li&gt;
&lt;li&gt;No rate limits, no vendor deprecation surprises&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ceph will punish you if you don't understand placement groups, replication, failure domains. A bad CRUSH map turns one dead disk into cluster-wide latency.&lt;/li&gt;
&lt;li&gt;OpenStack ships every 6 months. Skip releases and upgrades get harder, not easier.&lt;/li&gt;
&lt;li&gt;Engineers who can debug a flapping Ceph OSD or trace a Neutron issue through OVS are rare and expensive.&lt;/li&gt;
&lt;li&gt;Getting to production-ready is a multi-month project, not a weekend.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The managed cloud alternative
&lt;/h2&gt;

&lt;p&gt;This means AWS/Azure/GCP, EU alternatives like Scaleway/OVHcloud, or a managed infrastructure partner running the stack (sovereign or otherwise) on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compute/storage/managed DBs available in minutes&lt;/li&gt;
&lt;li&gt;Managed Kubernetes, managed DBs, managed LBs remove whole failure categories from your plate&lt;/li&gt;
&lt;li&gt;Autoscaling handles traffic spikes without manual capacity planning&lt;/li&gt;
&lt;li&gt;Multi-continent presence without building your own PoPs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Egress fees, IOPS charges, managed service premiums scale linearly or worse. We covered the real numbers in &lt;a href="https://binadit.com" rel="noopener noreferrer"&gt;cheap hosting vs managed cloud infrastructure&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;"EU region" doesn't mean sovereignty. A US-HQ'd provider's EU region still carries CLOUD Act exposure. More on that in our &lt;a href="https://binadit.com" rel="noopener noreferrer"&gt;EU region toggle piece&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Vendor controls the roadmap: pricing, deprecations, API changes happen on their schedule.&lt;/li&gt;
&lt;li&gt;You're boxed into whatever abstractions the provider gives you.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick decision framework
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dedicated_infra_engineers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;stay_on_managed_cloud&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;hire_a_partner_to_run_sovereign_stack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;vm_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;managed_cloud_or_simple_vps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# capex won't pay back fast enough
&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_the_numbers_on_sovereign_stack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# now it's worth modeling
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The honest takeaway: fewer than 3 dedicated infra engineers or under ~15 VMs means self-hosting Ceph/OpenStack is a risk you're probably underpricing. Above that threshold, the math starts favoring ownership, especially if data sovereignty is a hard requirement, not a nice-to-have.&lt;/p&gt;

&lt;p&gt;Full breakdown with the complete comparison table and framework: &lt;a href="https://binadit.com/blog/open-source-sovereign-stack-infrastructure-management-services-decision" rel="noopener noreferrer"&gt;read the original article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/open-source-sovereign-stack-infrastructure-management-services-decision" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
