<?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>Best practices for evaluating European alternatives to AWS, Azure and GCP</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 21 Oct 2026 07:19:24 +0000</pubDate>
      <link>https://dev.to/binadit/best-practices-for-evaluating-european-alternatives-to-aws-azure-and-gcp-477d</link>
      <guid>https://dev.to/binadit/best-practices-for-evaluating-european-alternatives-to-aws-azure-and-gcp-477d</guid>
      <description>&lt;h2&gt;
  
  
  Someone asked you to evaluate EU cloud alternatives. Here's how not to waste three sprints on it
&lt;/h2&gt;

&lt;p&gt;Your DPO, a customer, or procurement just asked whether you could run on European infrastructure instead of AWS, Azure or GCP. This isn't about picking a side in the sovereignty debate; it's about giving a straight technical answer. What actually breaks if you move, what's portable, and what needs real validation before anyone signs off.&lt;/p&gt;

&lt;p&gt;This comes from running the same evaluation repeatedly for SaaS teams, agencies and e-commerce platforms going through procurement cycles or security questionnaires. None of it requires ditching Kubernetes, Terraform or your CI/CD setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: "EU region" and "EU jurisdiction" get treated as the same thing
&lt;/h3&gt;

&lt;p&gt;They're not. AWS, Azure and GCP all run EU data center regions, but the parent company is still bound by US law, CLOUD Act included, no matter where the bytes sit. If the actual requirement is jurisdictional separation, flipping a region dropdown on a hyperscaler doesn't solve it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; write this distinction into your evaluation criteria before comparing a single vendor. Otherwise you'll spend weeks comparing the wrong thing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: you're comparing brands instead of primitives
&lt;/h3&gt;

&lt;p&gt;Most SaaS stacks need the same handful of things: compute, object storage, managed Postgres or MySQL, a load balancer, a CDN. List what you actually use, by capability, not by AWS product name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Example: workload inventory before evaluation
compute: 12 vCPU, autoscaling 2-8 nodes
object_storage: S3-compatible, 4TB, lifecycle rules
database: managed PostgreSQL 15, 200GB, daily PITR
cdn: edge caching, EU + UK pop coverage
queue: managed Redis, 2GB, persistence enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a vague "can we replace AWS" into an answerable question: which EU providers offer S3-compatible storage, managed Postgres with PITR, and a comparable autoscaling setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: assuming object storage means a rewrite
&lt;/h3&gt;

&lt;p&gt;It usually doesn't. OVHcloud, Scaleway, Exoscale and Hetzner (via third-party gateways) all expose S3-compatible APIs. If your app talks to S3 through the standard SDK, repointing the endpoint and credentials is often enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3 &lt;span class="nb"&gt;cp &lt;/span&gt;test.txt s3://my-bucket/test.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-url&lt;/span&gt; https://s3.de.io.cloud.ovh.net &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--profile&lt;/span&gt; eu-provider
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this smoke test before you plan anything bigger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 4: the database layer is where things actually go wrong
&lt;/h3&gt;

&lt;p&gt;Compute and object storage are portable. Managed databases are not, this is where EU providers diverge most: feature parity, replication tooling, backup retention defaults. Before committing to anything, run a full backup/restore cycle on the target provider and measure recovery time for real.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 5: latency assumptions based on a data center brochure
&lt;/h3&gt;

&lt;p&gt;Frankfurt or Amsterdam will serve EU traffic fine. If 20% of your users are in the UK, US or APAC, that's a different story. Run synthetic tests from multiple regions with &lt;code&gt;mtr&lt;/code&gt; or a distributed probe service, and look at p50/p95, not averages.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 6: CDN "EU" claims that don't match actual routing
&lt;/h3&gt;

&lt;p&gt;Some EU-headquartered CDN resellers still route certain regions through non-EU origin infrastructure. If sovereignty is a real requirement, get a documented PoP list and trace actual request paths yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"@curl-format.txt"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; https://your-cdn-endpoint.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or a plain traceroute from your target markets.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 7: certifications that apply to the reseller, not the facility
&lt;/h3&gt;

&lt;p&gt;Ask for the ISO 27001 (and NEN 7510 where relevant) of the physical data center, not the managed service layered on top. A logo on a website isn't a certificate. Request the actual scope statement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 8: pricing only the compute line
&lt;/h3&gt;

&lt;p&gt;European providers often look cheaper per vCPU hour, until you add egress, backup storage, IP allocation and support tiers. Build a like-for-like monthly cost model across at least three providers using your real usage pattern, not a generic instance size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 9: IaC that was never tested against the new provider
&lt;/h3&gt;

&lt;p&gt;If your Terraform targets AWS-specific resources, you usually need a provider swap, not a rewrite, for standard compute and networking. Run &lt;code&gt;terraform plan&lt;/code&gt; against a sandbox account early.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"openstack"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;auth_url&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ovh_auth_url&lt;/span&gt;
  &lt;span class="nx"&gt;domain_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt;
  &lt;span class="nx"&gt;tenant_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ovh_project_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"openstack_compute_instance_v2"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-node-1"&lt;/span&gt;
  &lt;span class="nx"&gt;flavor_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"b2-15"&lt;/span&gt;
  &lt;span class="nx"&gt;image_name&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Ubuntu 22.04"&lt;/span&gt;
  &lt;span class="nx"&gt;security_groups&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"default"&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;This catches unsupported resource types (certain autoscaling policies, specific VPC peering models) before they block you mid-migration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 10: treating the cutover as a single switch
&lt;/h3&gt;

&lt;p&gt;Run both environments in parallel with DNS-weighted traffic splitting. Validate error rates and latency at each traffic percentage. Keep the old environment warm through at least one full peak traffic cycle. This is standard practice for any zero-downtime migration, and it's what keeps a sovereignty decision from turning into an outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to actually roll this out
&lt;/h3&gt;

&lt;p&gt;Don't try to do all ten in one sprint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1 (1-2 weeks):&lt;/strong&gt; workload inventory and requirement separation. Pure documentation, gives procurement or legal something concrete to work with.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2 (2-4 weeks):&lt;/strong&gt; sandbox validation against at least two candidate providers. Runs in parallel with production, costs a few hundred euros in test resources, produces real numbers instead of vendor claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3 (4-10 weeks):&lt;/strong&gt; staged migration with IaC parity testing and a reversible cutover. Keep a rollback window of at least one full business cycle before decommissioning anything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Assign one engineer as technical owner from day one. Splitting this across the team produces benchmarks that don't line up later.&lt;/p&gt;

&lt;p&gt;None of this requires faith. It requires inventory, benchmarking and a staged rollout, the same discipline you'd apply to any infrastructure change.&lt;/p&gt;

&lt;p&gt;Full version with more context here: &lt;a href="https://binadit.com/blog/best-practices-private-cloud-infrastructure-eu-alternatives-aws-azure-gcp" rel="noopener noreferrer"&gt;Best practices for evaluating European alternatives to AWS, Azure and GCP&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/best-practices-private-cloud-infrastructure-eu-alternatives-aws-azure-gcp" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>security</category>
    </item>
    <item>
      <title>Why cheap hosting becomes expensive, and how managed cloud infrastructure fixes the math</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 16 Oct 2026 07:03:43 +0000</pubDate>
      <link>https://dev.to/binadit/why-cheap-hosting-becomes-expensive-and-how-managed-cloud-infrastructure-fixes-the-math-4p2k</link>
      <guid>https://dev.to/binadit/why-cheap-hosting-becomes-expensive-and-how-managed-cloud-infrastructure-fixes-the-math-4p2k</guid>
      <description>&lt;h2&gt;
  
  
  Your €30/month VPS is lying to you about its real cost
&lt;/h2&gt;

&lt;p&gt;Every engineer who has run a Friday-afternoon incident on a budget VPS knows this story. The invoice says €30. The actual bill includes a 20-minute checkout outage, a Slack thread full of panic, and three hours of your time spent figuring out why MySQL is refusing connections. None of that shows up on the hosting receipt. It shows up in your sprint velocity.&lt;/p&gt;

&lt;p&gt;This is the real pattern behind most migrations to managed infrastructure: the sticker price was never the full price. Let's break down why, and what actually fixes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why cheap hosting is a cost-shifting machine, not a cost-saving one
&lt;/h2&gt;

&lt;p&gt;Budget hosting plans work on oversubscription: sell the same CPU and memory pool to more tenants than could ever use it simultaneously. Fine, until your app actually needs what it was promised.&lt;/p&gt;

&lt;p&gt;Three failure modes show up constantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Noisy neighbors.&lt;/strong&gt; Shared I/O and CPU scheduling means someone else's batch job can wreck your p95 latency without you changing a single line of code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No headroom for peaks.&lt;/strong&gt; Plans are sized for average load. A 3x traffic spike from a launch or a viral post pushes memory past the limit, and the OOM killer starts picking off PHP-FPM workers or your database process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero engineering time baked in.&lt;/strong&gt; Nobody tunes &lt;code&gt;innodb_buffer_pool_size&lt;/code&gt;, rotates logs before disk fills, or watches your TLS cert expiry. That work doesn't vanish, it just gets dumped on whoever's on call, mid-incident instead of before one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The math, concretely
&lt;/h3&gt;

&lt;p&gt;A €30/month plan versus a properly sized €200/month managed VPS looks like €2,040/year in savings. Now add two checkout outages a year at 25 minutes each, on a store doing €500/day: roughly €520 in lost sales. Add 4-6 engineering hours to diagnose and recover at a €75/hour loaded cost: another €300-450. The "cheap" plan is already more expensive before you count what that engineer could've shipped instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the cost actually hides
&lt;/h2&gt;

&lt;p&gt;In migrations off budget hosting, the hidden cost clusters in three spots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Database performance degrading silently until one slow query takes the whole site down.&lt;/li&gt;
&lt;li&gt;No staging environment that resembles production, so customers find bugs before CI does.&lt;/li&gt;
&lt;li&gt;Manual TLS renewal that gets missed once a year and kills the whole domain for hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these are exotic problems. They're recurring operational work that a €30/month plan was never designed to cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: size for reality, automate the boring stuff, isolate the database
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Size compute to peak, not average
&lt;/h3&gt;

&lt;p&gt;Pull 90 days of CPU/memory metrics. No data? That's a signal on its own. Quick baseline 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;vmstat 2 10
free &lt;span class="nt"&gt;-m&lt;/span&gt;
mpstat &lt;span class="nt"&gt;-P&lt;/span&gt; ALL 1 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Target 60-70% utilization at peak, not 95%. If campaigns spike you 3x, provision for that or put autoscaling in front of it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Separate the database from the app tier
&lt;/h3&gt;

&lt;p&gt;On budget hosting, MySQL and your web server fight for the same resources. Splitting them onto two modest instances kills most noisy-neighbor issues. Baseline tuning for a dedicated MySQL 8 box with 8GB RAM:&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;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;innodb_buffer_pool_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5G&lt;/span&gt;
&lt;span class="py"&gt;innodb_log_file_size&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
&lt;span class="py"&gt;max_connections&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;150&lt;/span&gt;
&lt;span class="py"&gt;innodb_flush_log_at_trx_commit&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;query_cache_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buffer pool at roughly 60-70% of RAM, leaving room for connections and OS cache. On a 2GB plan this is simply not possible, which is the actual reason your 40ms query takes 900ms on shared hosting.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Automate what fails silently
&lt;/h3&gt;

&lt;p&gt;Cert expiry and disk-filling logs cause most "surprise" outages. Both fixable in under an hour:&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;# certbot auto-renewal via systemd timer, twice daily&lt;/span&gt;
certbot renew &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="nt"&gt;--deploy-hook&lt;/span&gt; &lt;span class="s2"&gt;"systemctl reload nginx"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Build staging that actually resembles production
&lt;/h3&gt;

&lt;p&gt;If staging is a subdirectory with debug mode flipped on, it's not catching production-class bugs. Match PHP version, DB engine version, and resource ratios (smaller is fine, different is not). Skip this and every deploy becomes a gamble.&lt;/p&gt;

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

&lt;p&gt;Don't trust "it feels faster." Check numbers before and after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Budget hosting&lt;/th&gt;
&lt;th&gt;Right-sized managed infra&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;P95 response time&lt;/td&gt;
&lt;td&gt;800-1500ms&lt;/td&gt;
&lt;td&gt;150-300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB query p95&lt;/td&gt;
&lt;td&gt;300-900ms&lt;/td&gt;
&lt;td&gt;20-60ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unplanned restarts/month&lt;/td&gt;
&lt;td&gt;2-5&lt;/td&gt;
&lt;td&gt;0-1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory headroom at peak&lt;/td&gt;
&lt;td&gt;&amp;lt;10%&lt;/td&gt;
&lt;td&gt;30-40%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Validation checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mysqladmin status&lt;/code&gt; to confirm no connection queueing under load&lt;/li&gt;
&lt;li&gt;Load test at 2-3x normal peak with k6, watch 5xx rates and latency trend, not just averages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;free -m&lt;/code&gt; during peak, zero swap usage, any swap activity means you're still undersized&lt;/li&gt;
&lt;li&gt;Measure TTFB separately from full page load, they diagnose different bottlenecks&lt;/li&gt;
&lt;li&gt;If you have incident history, track MTTR before/after; properly managed infra tends to shift this from hours to minutes because failover and monitoring are built in&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Keeping it fixed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Review capacity quarterly, not after an incident&lt;/li&gt;
&lt;li&gt;Treat SSL renewal, backups, and patching as code, not a human's memory&lt;/li&gt;
&lt;li&gt;Load test before big traffic events, not after an outage during one&lt;/li&gt;
&lt;li&gt;Isolate the database tier early, it's the first and most common contention point&lt;/li&gt;
&lt;li&gt;Keep staging close enough to production to catch real bugs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needs exotic tooling. It needs someone accountable for checking it on a schedule. That's usually the actual gap.&lt;/p&gt;

&lt;p&gt;Full writeup with more detail: &lt;a href="https://binadit.com/blog/why-cheap-hosting-becomes-expensive-managed-cloud-infrastructure" rel="noopener noreferrer"&gt;Why cheap hosting becomes expensive, and how managed cloud infrastructure fixes the math&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/why-cheap-hosting-becomes-expensive-managed-cloud-infrastructure" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>A 12-question sovereignty audit every DPO should run</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:15:10 +0000</pubDate>
      <link>https://dev.to/binadit/a-12-question-sovereignty-audit-every-dpo-should-run-3669</link>
      <guid>https://dev.to/binadit/a-12-question-sovereignty-audit-every-dpo-should-run-3669</guid>
      <description>&lt;h1&gt;
  
  
  Your vendor says 'no US subprocessors.' Here's how to actually check
&lt;/h1&gt;

&lt;p&gt;Your DPO just forwarded you another vendor questionnaire with a checked box next to "no US subprocessors." Before you forward it back with a thumbs up, know this: most of those boxes get checked optimistically, not accurately. If you're the engineer who ends up answering "where does our data actually live" during procurement calls, this list is for you.&lt;/p&gt;

&lt;p&gt;The gap is rarely malicious. It's usually a monitoring tool that defaulted to a US region, a backup target nobody revisited after setup, or a subprocessor that's legally based in the EU but owned by a US parent. Sovereignty audits exist to find exactly that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why yes/no questionnaires fail
&lt;/h2&gt;

&lt;p&gt;A vendor questionnaire asks "do you store data in the EU?" and gets a yes. It doesn't ask who holds the encryption keys, where support engineers sit, or whether the CDN in front of the app routes through a US edge network. Evidence beats assertions every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checks that actually matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Subprocessor list freshness.&lt;/strong&gt; If a vendor's public subprocessor list hasn't changed in over a year despite shipping new features, it's probably stale, not accurate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parent company ownership, not just contracting entity.&lt;/strong&gt; A subprocessor incorporated in Germany can still be a wholly owned US subsidiary, which pulls it under the CLOUD Act regardless of server location. Always ask for the ultimate parent, not the signing entity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Support and escalation location.&lt;/strong&gt; If a US-based support engineer can remote into production to debug something, that's a data access path your residency clause doesn't cover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring and APM defaults.&lt;/strong&gt; Sentry, Datadog, and New Relic all offer EU hosting, but it's opt-in. Primary infra can be EU-only while telemetry leaks through a third-party integration nobody checked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN and DNS routing.&lt;/strong&gt; A CDN is a subprocessor too. Origin in Frankfurt doesn't mean much if the CDN is a US hyperscaler's edge network. You can verify this yourself:&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;# Check which edge network is actually serving a domain&lt;/span&gt;
dig +short CNAME cdn.example.com

&lt;span class="c"&gt;# Trace the resolved IP's registered location&lt;/span&gt;
whois &lt;span class="si"&gt;$(&lt;/span&gt;dig +short cdn.example.com | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This won't give you a legal answer, but it'll tell you fast whether the CDN claim matches DNS reality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Backup and DR region.&lt;/strong&gt; EU-only production paired with a US backup target is common, usually because backup tooling defaulted there during setup and nobody revisited it. Ask explicitly where backups, snapshots, and DR replicas live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who holds the encryption keys, and from where.&lt;/strong&gt; Encryption at rest is table stakes. If a US entity or cloud provider holds the keys, EU residency doesn't block a lawful access request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legal basis for remaining transfers.&lt;/strong&gt; If zero international transfers isn't achievable, find out if they're relying on SCCs, BCRs, or something else, and whether it's been tested against post-Schrems II expectations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documented incident response for law enforcement requests.&lt;/strong&gt; Ask what happens procedurally if a non-EU authority requests data. If there's no written process, there's no real process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether the infra partner's own tooling is sovereign.&lt;/strong&gt; Some vendors sell you an EU-region sovereignty story while their own ticketing or remote access tooling runs on US SaaS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failover and migration behavior.&lt;/strong&gt; Sovereignty guarantees are easiest to break during a DR drill or fast migration. Ask the vendor to walk through a specific failover scenario and confirm the DR target still meets the same residency commitments as production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct technical contact.&lt;/strong&gt; Can someone answer these questions in a conversation, or does everything route through a ticket queue with a copy-pasted compliance statement three days later? The former usually means they understand their own subprocessor chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get the real subprocessor table, not a summary
&lt;/h2&gt;

&lt;p&gt;Ask for the DPA's actual subprocessor annex. It should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Subprocessor      | Purpose              | Entity location    | Parent HQ
EU hosting partner| Infrastructure       | Netherlands        | Netherlands
EU email relay    | Transactional email  | Germany             | Germany
Monitoring vendor | APM / error tracking | EU region (opt-in)  | United States
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That third row fails a strict no-US-subprocessor policy even though the data is technically hosted in the EU. The parent HQ column is the one most vendor tables conveniently leave out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rolling this out without reopening every contract at once
&lt;/h2&gt;

&lt;p&gt;Most teams have ten to thirty vendors already under contract. Don't try to audit all of them simultaneously.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tier vendors by how much regulated or personal data they touch&lt;/li&gt;
&lt;li&gt;Run the full check against top-tier vendors first: core infra, email/identity, anything handling payment or health data&lt;/li&gt;
&lt;li&gt;Bake the subprocessor table requirement into procurement for new vendors from day one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full details and context: &lt;a href="https://binadit.com/blog/no-us-subprocessor-sovereignty-audit-dpo-checklist" rel="noopener noreferrer"&gt;the original sovereignty audit checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/no-us-subprocessor-sovereignty-audit-dpo-checklist" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>privacy</category>
      <category>security</category>
    </item>
    <item>
      <title>How to automate SSL certificate renewal across a fleet of servers</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:44:08 +0000</pubDate>
      <link>https://dev.to/binadit/how-to-automate-ssl-certificate-renewal-across-a-fleet-of-servers-1k0c</link>
      <guid>https://dev.to/binadit/how-to-automate-ssl-certificate-renewal-across-a-fleet-of-servers-1k0c</guid>
      <description>&lt;h1&gt;
  
  
  Stop babysitting SSL certs: automate renewal across your whole fleet
&lt;/h1&gt;

&lt;p&gt;If someone on your team still has a recurring calendar reminder to check certificate expiry dates, you have a problem worth fixing today. Not next quarter, today. Manual cert tracking across multiple hosts is the kind of low-grade recurring work that quietly eats hours every month and never shows up as a line item until something expires in production at 2am.&lt;/p&gt;

&lt;p&gt;This guide walks through building fleet-wide automated renewal using Let's Encrypt, the ACME protocol, and a central orchestration pattern. It works whether you have 5 boxes or 500, behind a load balancer, as separate app nodes, or spread across regions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assumptions before you start
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You control DNS (or can hit a DNS API) for Cloudflare, Route 53, DigitalOcean, etc.&lt;/li&gt;
&lt;li&gt;Linux servers (Ubuntu 22.04/24.04 in examples, distro-agnostic approach)&lt;/li&gt;
&lt;li&gt;SSH access across the fleet, ideally via Ansible or Salt, not manual loops&lt;/li&gt;
&lt;li&gt;TLS termination happens at Nginx or HAProxy, not inside the app itself&lt;/li&gt;
&lt;li&gt;You're fine with cron or systemd timers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll use certbot since it has the broadest plugin support, though acme.sh or lego work fine with the same architecture if you want something lighter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pick a validation method that actually scales
&lt;/h2&gt;

&lt;p&gt;HTTP-01 validation needs port 80 reachable on the exact host being validated. That's fine for one server. It falls apart across a fleet behind a load balancer, where traffic for a domain doesn't predictably land on the same box twice.&lt;/p&gt;

&lt;p&gt;DNS-01 is the better default. No inbound traffic required, supports wildcards, and lets you centralize issuance on one host even if the cert gets distributed everywhere else.&lt;/p&gt;

&lt;p&gt;Cloudflare example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-dns-cloudflare &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/letsencrypt/cloudflare.ini &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
dns_cloudflare_api_token = YOUR_TOKEN
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /etc/letsencrypt/cloudflare.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Centralize issuance, distribute the result
&lt;/h2&gt;

&lt;p&gt;Don't run certbot independently on every server. That gives you inconsistent cert state across the fleet and more surfaces that can silently fail. Issue once, push everywhere.&lt;/p&gt;

&lt;p&gt;Use a deploy hook to push the renewed cert out via your config management tool:&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="nb"&gt;cp&lt;/span&gt; /etc/letsencrypt/live/example.com/fullchain.pem /etc/haproxy/certs/example.com.pem
systemctl reload haproxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep that logic in one deploy hook script, not scattered across playbooks. One script, one source of truth for what happens post-renewal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't forget the reload
&lt;/h2&gt;

&lt;p&gt;Copying a new cert file does nothing if the service is still holding the old one in memory. Every distribution step needs a paired reload handler, Nginx or HAProxy, whichever terminates TLS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alert on failure, not just on success
&lt;/h2&gt;

&lt;p&gt;A cron job that fails silently for a month isn't automation, it's a delayed incident waiting to happen. Wrap the renewal and check the exit 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;#!/bin/bash&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; certbot renew &lt;span class="nt"&gt;--quiet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://hooks.slack.com/services/YOUR/WEBHOOK/URL &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"text":"Certbot renewal failed on '&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;'"}'&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the webhook for whatever your team already uses, PagerDuty, Opsgenie, Slack. The point is a human sees the failure within a day, not when the cert is about to expire.&lt;/p&gt;

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

&lt;p&gt;Don't trust the cron job exists and assume it's fine. Check end to end.&lt;/p&gt;

&lt;p&gt;Dry run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check expiry across the fleet:&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="k"&gt;for &lt;/span&gt;host &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;fleet_hosts.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;: "&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-connect&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;:443 &lt;span class="nt"&gt;-servername&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-enddate&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better yet, wire cert expiry into your existing monitoring. Prometheus blackbox exporter, Uptime Kuma, Zabbix all support it natively. Alert when &lt;code&gt;probe_ssl_earliest_cert_expiry&lt;/code&gt; drops below 14 days, this is your independent confirmation layer outside certbot's own logs.&lt;/p&gt;

&lt;p&gt;And confirm distribution reached every node, not just the one that ran certbot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible web_fleet &lt;span class="nt"&gt;-m&lt;/span&gt; shell &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="s2"&gt;"openssl x509 -in /etc/ssl/certs/example.com.pem -noout -enddate"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mismatched expiry date across hosts means distribution failed somewhere, which is a more common failure mode than renewal itself breaking.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Hardcoding API tokens in playbooks instead of using a secrets manager&lt;/li&gt;
&lt;li&gt;Forgetting the service reload after distribution&lt;/li&gt;
&lt;li&gt;Hitting Let's Encrypt's rate limit (50 certs per registered domain per week), easy to hit if you're doing per-subdomain certs instead of a wildcard&lt;/li&gt;
&lt;li&gt;Running certbot independently per server instead of centralizing&lt;/li&gt;
&lt;li&gt;Shipping automation with zero failure alerting&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this goes next
&lt;/h2&gt;

&lt;p&gt;The same pattern, central issuance plus fleet-wide distribution via config management, extends naturally to SSH key rotation, firewall rule pushes, and post-migration config redeploys. If you're running this at scale, folding it into a GitOps pipeline means one less automation system to monitor separately from your app deployments.&lt;/p&gt;

&lt;p&gt;Full original writeup with more detail: &lt;a href="https://binadit.com/blog/automate-ssl-certificate-renewal-cloud-cost-optimization-services" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/automate-ssl-certificate-renewal-cloud-cost-optimization-services" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>sslautomation</category>
      <category>letsencrypt</category>
      <category>devops</category>
      <category>serverfleetmanagement</category>
    </item>
    <item>
      <title>Understanding what an Outlook alternative actually needs to do to keep mail in Europe</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 07 Oct 2026 09:43:02 +0000</pubDate>
      <link>https://dev.to/binadit/understanding-what-an-outlook-alternative-actually-needs-to-do-to-keep-mail-in-europe-460c</link>
      <guid>https://dev.to/binadit/understanding-what-an-outlook-alternative-actually-needs-to-do-to-keep-mail-in-europe-460c</guid>
      <description>&lt;h2&gt;
  
  
  Switching your inbox region doesn't change who owns the data
&lt;/h2&gt;

&lt;p&gt;If you've ever ticked "EU data region" in Microsoft 365 or Google Workspace and considered the jurisdiction problem solved, here's the uncomfortable part: it isn't. The US CLOUD Act (2018) says a provider under US jurisdiction must hand over data under its possession, custody or control when a US court orders it, regardless of which country the servers physically sit in. The law tracks the company, not the data centre. Let's get into why that matters for anyone planning a real outlook alternative, and what actually has to change under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  The jurisdiction problem, in plain terms
&lt;/h3&gt;

&lt;p&gt;Outlook is the client. Microsoft 365 is the service. For most orgs those have merged into one experience, but legally they're two different things, and the service is what matters here.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft Corporation: American company, subject to US law&lt;/li&gt;
&lt;li&gt;Google LLC: American company, subject to US law&lt;/li&gt;
&lt;li&gt;CLOUD Act: applies based on &lt;em&gt;custody&lt;/em&gt;, not server location&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Meanwhile GDPR Chapter V (Articles 44-50) restricts personal data leaving the EEA without an adequacy decision or safeguards like SCCs. Article 48 is the key detail most teams miss: a foreign court order doesn't automatically make a transfer lawful. Article 24 puts the compliance burden on you, the controller, not the vendor.&lt;/p&gt;

&lt;p&gt;These two legal regimes don't reconcile cleanly, and the courts have said so three times:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Safe Harbor, struck down by the CJEU in 2015&lt;/li&gt;
&lt;li&gt;Privacy Shield, struck down in Schrems II (July 2020), over FISA Section 702 giving EU citizens no real remedy against US government access&lt;/li&gt;
&lt;li&gt;EU-US Data Privacy Framework, adopted 10 July 2023, currently in force, but under active challenge: dismissed by the EU General Court on 3 September 2025, appealed 31 October 2025, and now pending before the CJEU&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want the sworn-testimony version of this: on 10 June 2025, Microsoft France's director of public and legal affairs, Anton Carniaux, was asked under oath before a French Senate inquiry whether he could guarantee French citizens' data would never be handed to the US government without French authorisation. His answer: "Non, je ne peux pas le garantir" ("No, I cannot guarantee that"), while noting it hadn't happened so far. No guarantee, no known incident. Both halves matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the exposure actually lives in the mail pipeline
&lt;/h3&gt;

&lt;p&gt;When mail moves through Microsoft 365, it typically passes through multiple Microsoft-controlled systems, each one a separate copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Send -&amp;gt; Exchange Online mailbox
     -&amp;gt; Spam/malware filtering
     -&amp;gt; Journaling/compliance archive
     -&amp;gt; Legal hold / eDiscovery store (if applicable)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those is under Microsoft's custody regardless of region settings. Picking an EU data centre changes the postal address of the hardware. It does not change who has custody.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a real migration actually requires
&lt;/h3&gt;

&lt;p&gt;Say you're moving a 40-person org off Microsoft 365: Outlook, OneDrive, Teams, plus a shared &lt;code&gt;info@&lt;/code&gt; mailbox watched by three people. Here's the checklist, piece by piece:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Protocols the new provider must support:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IMAP / SMTP   -&amp;gt; mail
CalDAV        -&amp;gt; calendar
CardDAV       -&amp;gt; contacts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the replacement is web-app-only with no protocol support, you've just swapped one lock-in for another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared mailbox permissions:&lt;/strong&gt; &lt;code&gt;info@&lt;/code&gt; needs multi-user access control without issuing separate logins per person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS/auth records to rebuild:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SPF record  - sender authorization
DKIM record - message signing
DMARC record - policy enforcement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get these wrong and your outgoing mail gets flagged as spam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client UX:&lt;/strong&gt; People are used to the three-pane layout (folders / message list / reading pane). Force everyone into a browser tab and adoption drops, independent of how solid the backend is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File sync:&lt;/strong&gt; Needs to replicate "files on demand" behavior, files show as available without consuming local disk until opened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meetings:&lt;/strong&gt; Guests need to join via browser link, no install required, since you can't mandate your internal tooling on every client or supplier.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Binadit Workspace addresses this
&lt;/h3&gt;

&lt;p&gt;Binadit Workspace covers that full checklist as one product, one login, on the customer's own domain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mail/calendar/contacts over IMAP, SMTP, CalDAV, CardDAV, and JMAP, so existing clients keep working&lt;/li&gt;
&lt;li&gt;Shared mailboxes with per-colleague permissions&lt;/li&gt;
&lt;li&gt;DNS records (MX, SPF, DKIM, DMARC, autoconfig) placed automatically via Binadit's own DNS tool&lt;/li&gt;
&lt;li&gt;Northmail: free macOS/Windows mail client in the familiar three-pane layout, connects to Workspace, on-prem Exchange, and Gmail simultaneously, useful during phased migrations&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;An outlook alternative isn't about finding a nicer inbox UI. It's about finding a provider under a different legal jurisdiction, one that actually replaces the full protocol stack your org depends on, not just the icon on the desktop.&lt;/p&gt;

&lt;p&gt;Full writeup with the legal citations: &lt;a href="https://binadit.com/blog/outlook-alternative-keeps-mail-in-europe" rel="noopener noreferrer"&gt;https://binadit.com/blog/outlook-alternative-keeps-mail-in-europe&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/outlook-alternative-keeps-mail-in-europe" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>outlookalternative</category>
      <category>emailsovereignty</category>
      <category>gdpremailhosting</category>
      <category>cloudact</category>
    </item>
    <item>
      <title>How to set up website server performance that holds under real traffic</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:53:52 +0000</pubDate>
      <link>https://dev.to/binadit/how-to-set-up-website-server-performance-that-holds-under-real-traffic-31c5</link>
      <guid>https://dev.to/binadit/how-to-set-up-website-server-performance-that-holds-under-real-traffic-31c5</guid>
      <description>&lt;h2&gt;
  
  
  Your server works fine until it doesn't
&lt;/h2&gt;

&lt;p&gt;Every infrastructure engineer knows this moment: the server hums along fine in staging, then a traffic spike hits production and everything falls over. Requests queue, workers pin at max, response times spike from 200ms to 8 seconds. The usual reaction is to throw more CPU at it. That's rarely the actual problem.&lt;/p&gt;

&lt;p&gt;Most servers that collapse under load aren't under-resourced, they're misconfigured. The default settings for PHP-FPM, Nginx, and your database connections were never meant for production traffic. Here's the exact sequence of changes that fixes this, in the order that actually matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 0: measure before you touch anything
&lt;/h2&gt;

&lt;p&gt;You can't prove a fix worked without a baseline. Run this first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ab &lt;span class="nt"&gt;-n&lt;/span&gt; 1000 &lt;span class="nt"&gt;-c&lt;/span&gt; 50 https://yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Record requests per second, mean response time, and failed request count. Check &lt;code&gt;uptime&lt;/code&gt; and &lt;code&gt;vmstat 1 5&lt;/code&gt; during the test too. Write it all down; you'll compare against it later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 1: PHP-FPM is probably misconfigured
&lt;/h2&gt;

&lt;p&gt;The default pool config is not production-ready. Open it:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/php/8.3/fpm/pool.d/www.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Size your workers based on actual memory usage, not guesswork. Check real footprint per worker with &lt;code&gt;ps aux | grep php-fpm&lt;/code&gt; (typically 30-60MB for WordPress/Laravel), then divide available RAM by that number.&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;40&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;15&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 recycles workers periodically, which stops slow memory leaks from degrading your server over hours of uptime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 2: Nginx is dropping connections it doesn't need to
&lt;/h2&gt;

&lt;p&gt;Reduce TCP handshake overhead with proper keepalive and connection settings:&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;worker_processes&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;worker_rlimit_nofile&lt;/span&gt; &lt;span class="mi"&gt;65535&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;worker_connections&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="s"&gt;epoll&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;multi_accept&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive_timeout&lt;/span&gt; &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive_requests&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;sendfile&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;tcp_nopush&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;tcp_nodelay&lt;/span&gt; &lt;span class="no"&gt;on&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;If you're proxying to PHP-FPM over a Unix socket, bump the backlog to match expected concurrency:&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;listen&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/run/php/php8.3-fpm.sock&lt;/span&gt;
&lt;span class="py"&gt;listen.backlog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1024&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fix 3: your database connections are too expensive
&lt;/h2&gt;

&lt;p&gt;Each unpooled connection costs a handshake, auth round trip, and memory allocation. At scale, this is a silent killer. For Postgres, PgBouncer is the standard fix:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;pgbouncer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;yourapp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=5432 dbname=yourapp&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;listen_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;6432&lt;/span&gt;
&lt;span class="py"&gt;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;md5&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point your app at port 6432 instead of 5432. This typically cuts connection overhead by 60-80% under concurrent load. MySQL users: look at ProxySQL or mysqlnd_mux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fix 4: cache what shouldn't hit the database every time
&lt;/h2&gt;

&lt;p&gt;Install Redis:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;redis-server
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;redis-server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set eviction policy so it doesn't just run out of memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;maxmemory&lt;/span&gt; &lt;span class="err"&gt;512mb&lt;/span&gt;
&lt;span class="err"&gt;maxmemory-policy&lt;/span&gt; &lt;span class="err"&gt;allkeys-lru&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For custom apps, wrap expensive queries in a cache-aside pattern:&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;$cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"product:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&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="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SELECT * FROM products WHERE id = ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$id&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;setex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&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;
  
  
  Fix 5: stop static assets from hitting your app server
&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;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(jpg|jpeg|png|gif|css|js|woff2)&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;If there's a CDN in front, confirm it respects your origin headers instead of overriding with its own default TTL.&lt;/p&gt;

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

&lt;p&gt;Re-run the same load test, same concurrency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ab &lt;span class="nt"&gt;-n&lt;/span&gt; 1000 &lt;span class="nt"&gt;-c&lt;/span&gt; 50 https://yourdomain.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare against baseline. RPS should climb, often 30-100%. Response time variance between p50 and p95 should tighten. Failed requests should hit zero at your target concurrency.&lt;/p&gt;

&lt;p&gt;Watch PHP-FPM workers scale during load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;watch &lt;span class="nt"&gt;-n&lt;/span&gt; 1 &lt;span class="s1"&gt;'ps aux | grep php-fpm | wc -l'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers should scale up smoothly and settle, not spike immediately to &lt;code&gt;max_children&lt;/code&gt; and stay there. If they pin instantly, either your sizing is wrong or a slow query is holding connections open too long.&lt;/p&gt;

&lt;p&gt;Check PgBouncer is actually in the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="nt"&gt;-h&lt;/span&gt; 127.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; 6432 &lt;span class="nt"&gt;-U&lt;/span&gt; youruser yourapp &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SHOW POOLS;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And check Redis hit rate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;redis-cli info stats | &lt;span class="nb"&gt;grep &lt;/span&gt;keyspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Healthy cache-aside logic should show above 80% hit ratio within minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes that undo all of this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Oversizing max_children.&lt;/strong&gt; More workers than RAM supports causes swapping, which is worse than queued requests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching without invalidation.&lt;/strong&gt; Stale cached data is a harder bug to catch than a slow endpoint. Always set a TTL or invalidate on write.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pooling without adjusting app-level assumptions.&lt;/strong&gt; Transaction-mode pooling breaks session-level features like advisory locks if your app still expects a persistent connection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the baseline.&lt;/strong&gt; No before/after numbers means no proof anything worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a full re-architecture. It's five configuration changes applied in order, with measurement at each step. That's usually enough to take a server from falling over at the first spike to handling concurrency predictably.&lt;/p&gt;

&lt;p&gt;Full walkthrough with more context: &lt;a href="https://binadit.com/blog/website-server-performance-managed-cloud-provider-europe-setup" rel="noopener noreferrer"&gt;How to set up website server performance that holds under real traffic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/website-server-performance-managed-cloud-provider-europe-setup" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Your email is GDPR-compliant today. Will it still be next year?</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:07:11 +0000</pubDate>
      <link>https://dev.to/binadit/your-email-is-gdpr-compliant-today-will-it-still-be-next-year-3o5b</link>
      <guid>https://dev.to/binadit/your-email-is-gdpr-compliant-today-will-it-still-be-next-year-3o5b</guid>
      <description>&lt;h2&gt;
  
  
  Your GDPR compliance depends on a court case you're not tracking
&lt;/h2&gt;

&lt;p&gt;Here's an uncomfortable fact: the legal basis for your Google Workspace or Microsoft 365 email is a single framework that's currently being challenged in front of the same court that already killed its two predecessors. If you run infrastructure or make hosting decisions, this is worth ten minutes of your time.&lt;/p&gt;

&lt;p&gt;Ask your IT partner if EU email in a US productivity suite is GDPR-compliant and you'll get a confident yes: both providers are certified under the EU-US Data Privacy Framework. Technically true. Also missing the point.&lt;/p&gt;

&lt;h3&gt;
  
  
  The legal stack, in short
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sending personal data to a US company is a "transfer" under GDPR Chapter V (Articles 44-50).&lt;/li&gt;
&lt;li&gt;That's only lawful today because of the EU-US Data Privacy Framework (DPF), adopted July 2023.&lt;/li&gt;
&lt;li&gt;Its two predecessors, Safe Harbor and Privacy Shield, were both struck down by the Court of Justice of the EU (CJEU).&lt;/li&gt;
&lt;li&gt;The DPF is now under appeal at that same court.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three attempts. Two collapses. One pending verdict. That's not a stable foundation to build a compliance program on.&lt;/p&gt;

&lt;h3&gt;
  
  
  "We use an EU data center" doesn't fix it
&lt;/h3&gt;

&lt;p&gt;This is the part engineers get wrong most often. Region selection is not jurisdiction. Under the US CLOUD Act, a US company has to hand over data it controls when a US authority demands it, regardless of which data center it's sitting in. A Frankfurt or Amsterdam region owned by a US parent is still in scope.&lt;/p&gt;

&lt;p&gt;Don't take my word for it. In June 2025, Microsoft France's director of legal affairs testified under oath before a French Senate inquiry. Asked directly if he could guarantee French citizens' data would never be handed to the US government without French authorization, his answer was: "No, I cannot guarantee that."&lt;/p&gt;

&lt;p&gt;That's the provider, on the record, under oath.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where things stand right now
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A challenge to the DPF (Latombe v. Commission) was dismissed by the General Court in September 2025, but only on facts as they existed in 2023. Nothing since then was considered. It's now on appeal to the CJEU.&lt;/li&gt;
&lt;li&gt;Microsoft was granted leave to intervene in that appeal. A company doesn't intervene in EU court proceedings unless it has serious skin in the game.&lt;/li&gt;
&lt;li&gt;The US oversight board that underpins the DPF (PCLOB) has been without quorum since January 2025.&lt;/li&gt;
&lt;li&gt;In June 2026, the US Supreme Court ruled FTC commissioners can be removed at will by the President. The FTC is the enforcement body for the DPF on the US side.&lt;/li&gt;
&lt;li&gt;FISA Section 702, the surveillance law at the center of the original Schrems II ruling, lapsed in June 2026 after failed extensions. Collection continues under existing court certifications regardless.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this means the DPF will definitely fall. It means your compliance posture depends on a decision you don't control, on a timeline you can't predict. Last time this happened (Schrems II, July 2020), companies had zero notice and a three-year gap before a replacement existed.&lt;/p&gt;

&lt;h3&gt;
  
  
  The AI layer adds more surface area
&lt;/h3&gt;

&lt;p&gt;Copilot and Gemini are now embedded directly in mail and file storage. That's more data flows to account for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Questions worth asking your provider contract, literally:&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Does input get used for model training? (check tier, not marketing page)
&lt;span class="p"&gt;-&lt;/span&gt; Can you delete a specific individual's data from a trained model? (usually no)
&lt;span class="p"&gt;-&lt;/span&gt; Do you know which model processed which record, in which region?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The EU AI Act's transparency rules are already active (since August 2026). The heavier documentation requirements for HR, credit, and access-to-services use cases were pushed to December 2027, but that's a delay, not a way out. If you can't answer the questions above today, you won't be able to answer them then either.&lt;/p&gt;

&lt;h3&gt;
  
  
  The actual fix
&lt;/h3&gt;

&lt;p&gt;If email, calendar, and file storage sit with a provider incorporated and operating entirely in the EU, no CLOUD Act exposure exists because there's no US legal entity to compel. That's not a mitigation, it's the absence of the transfer problem entirely.&lt;/p&gt;

&lt;p&gt;As a data point: a Dutch provider offering this setup starts at €1.99 per mailbox per month, often cheaper than what teams already pay for Workspace or M365 licensing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical takeaway
&lt;/h3&gt;

&lt;p&gt;Check who actually handles mail for your domain (MX records will tell you the provider, not the jurisdiction, but it's a start). Then ask your provider the same question the French Senate asked Microsoft: can you guarantee this data is never handed over without our government's consent? If the answer is "no, but it hasn't happened," that's not a compliance guarantee, that's a probability bet.&lt;/p&gt;

&lt;p&gt;Full breakdown of the legal timeline and case law: &lt;a href="https://binadit.com/blog/email-gdpr-compliant-google-workspace-microsoft-365" rel="noopener noreferrer"&gt;Your email is GDPR-compliant today. Will it still be next year?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/email-gdpr-compliant-google-workspace-microsoft-365" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gdpr</category>
      <category>googleworkspace</category>
      <category>microsoft365</category>
      <category>cloudact</category>
    </item>
    <item>
      <title>Solving the deploy frequency wall: from weekly releases to multiple daily deploys without new incidents</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Fri, 18 Sep 2026 07:59:30 +0000</pubDate>
      <link>https://dev.to/binadit/solving-the-deploy-frequency-wall-from-weekly-releases-to-multiple-daily-deploys-without-new-4i8h</link>
      <guid>https://dev.to/binadit/solving-the-deploy-frequency-wall-from-weekly-releases-to-multiple-daily-deploys-without-new-4i8h</guid>
      <description>&lt;h1&gt;
  
  
  Why your team keeps reverting to weekly deploys (and how to actually fix it)
&lt;/h1&gt;

&lt;p&gt;Here's a pattern that shows up constantly in teams of 15-40 engineers: leadership pushes for faster releases, the team bumps deploy frequency up, and within a few weeks someone quietly walks it back to weekly. No mandate, no meeting. Just two or three rough releases in a row, and the team self-polices back to "safe" cadence.&lt;/p&gt;

&lt;p&gt;It looks like a discipline problem. It isn't. It's an infrastructure problem, and it's fixable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real issue: your pipeline was built for a different era
&lt;/h2&gt;

&lt;p&gt;When you deploy weekly, you can afford sloppiness because a human has time to babysit each release. Watch the error rate for 20 minutes, glance at a dashboard, ship it. Try that five times a day and the manual check either disappears or becomes theater nobody trusts.&lt;/p&gt;

&lt;p&gt;Under the hood, this usually comes down to four coupled problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deploys aren't atomic.&lt;/strong&gt; Code, migrations, and config all ship in one shot. If the migration is slow, the code can't go out without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback means redeploying.&lt;/strong&gt; Reverting takes the same pipeline, the same time, the same risk as deploying forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring lags deploy speed.&lt;/strong&gt; Alert thresholds and log delays were tuned for hourly checks, not minute-by-minute validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blast radius is 100%.&lt;/strong&gt; Every deploy hits the whole fleet at once, so any bad release is automatically a full incident.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is about code quality or test coverage. It's about the deploy path, routing layer, and monitoring stack not being built for the frequency you're asking of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: decouple, automate rollback, shrink the blast radius
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Split schema changes from code deploys
&lt;/h3&gt;

&lt;p&gt;Migrations are usually the biggest source of deploy anxiety. Use the expand/contract pattern so schema changes never ship in the same step as code:&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 (safe, backward compatible)&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;shipping_method_v2&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;50&lt;/span&gt;&lt;span class="p"&gt;)&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;-- Step 2: dual-write in app code, backfill&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;shipping_method_v2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shipping_method&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;shipping_method_v2&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;-- Step 3: contract (separate deploy, days later)&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;shipping_method&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Code deploys stop waiting on migrations, and migrations stop blocking rollbacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Make rollback a routing flip, not a redeploy
&lt;/h3&gt;

&lt;p&gt;If reverting means rerunning the pipeline backward, people will hesitate to ship under pressure. Rollback should be a traffic switch, done in seconds:&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 upstream weight shift for canary rollback&lt;/span&gt;
&lt;span class="k"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;backend&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;app-v124-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="s"&gt;weight=0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;# new version, was live&lt;/span&gt;
    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;app-v123-1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt; &lt;span class="s"&gt;weight=100&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;# previous stable, restored&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# reload takes effect in under 1 second&lt;/span&gt;
&lt;span class="k"&gt;nginx&lt;/span&gt; &lt;span class="s"&gt;-s&lt;/span&gt; &lt;span class="s"&gt;reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on Kubernetes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout undo deployment/checkout-api
kubectl rollout status deployment/checkout-api &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;30s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship to a small slice of instances first. Healthy? Shift more weight over. Not healthy? Shift back to zero. No twelve-minute rebuild required to recover.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Automate the canary check
&lt;/h3&gt;

&lt;p&gt;A human staring at Grafana for 20 minutes works at weekly cadence. At five deploys a day, it's neither sustainable nor reliable, people get worse at spotting anomalies the more times they repeat the check.&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;canary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;error_rate&lt;/span&gt;
      &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline + 0.5%&lt;/span&gt;
      &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;p95_latency&lt;/span&gt;
      &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline + 15%&lt;/span&gt;
      &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5xx_count&lt;/span&gt;
      &lt;span class="na"&gt;threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;baseline + &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
      &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
  &lt;span class="na"&gt;action_on_fail&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto_rollback&lt;/span&gt;
  &lt;span class="na"&gt;promotion_steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;5%&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;25%&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;50%&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;100%&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;step_duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is probably the single highest-leverage change for teams moving to daily deploys. It turns a subjective judgment call into a repeatable gate.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Shrink the blast radius with progressive rollout
&lt;/h3&gt;

&lt;p&gt;Deploying to 100% of instances at once makes every release a full-fleet bet. Instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ship to 5% of instances or one availability zone&lt;/li&gt;
&lt;li&gt;Hold through a full traffic cycle, including cron jobs and batch work&lt;/li&gt;
&lt;li&gt;Auto-promote on healthy metrics, auto-rollback on bad ones&lt;/li&gt;
&lt;li&gt;Only reach 100% after clearing every stage&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same principle as safe database migrations: keep each irreversible step small.&lt;/p&gt;

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

&lt;p&gt;Don't just track "deploy success." Watch these specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Change failure rate&lt;/strong&gt;: percentage of deploys triggering a rollback or hotfix. Aim under 15% (DORA elite benchmark), trending down as frequency goes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MTTR&lt;/strong&gt;: with automated rollback via traffic weight shift, this should drop under 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback execution time&lt;/strong&gt;: measure from "threshold breached" to "traffic fully reverted." Should be seconds to low minutes, not tied to build time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canary false negative rate&lt;/strong&gt;: how often something bad slips past the gate and gets caught by users instead. If it happens more than once a quarter, retune thresholds, don't remove the gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deploy frequency vs. incident count&lt;/strong&gt;: these two lines should decouple. If incidents still climb with frequency, your canary metrics are probably missing a business signal like checkout completion or cart abandonment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give this at least four weeks at the new cadence before declaring victory. One good week proves nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it from backsliding
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Alert on deploy frequency itself. A quiet drop without an explicit decision usually means the pipeline is causing avoidance.&lt;/li&gt;
&lt;li&gt;Treat canary thresholds as living config, not a one-time setup. Traffic patterns shift, thresholds need to shift with them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full technical breakdown, including the zero-downtime migration mechanics: &lt;a href="https://binadit.com/blog/infrastructure-performance-optimization-multiple-daily-deploys" 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/infrastructure-performance-optimization-multiple-daily-deploys" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to stabilize a system while it is actively failing</title>
      <dc:creator>binadit</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:32:01 +0000</pubDate>
      <link>https://dev.to/binadit/how-to-stabilize-a-system-while-it-is-actively-failing-2ph3</link>
      <guid>https://dev.to/binadit/how-to-stabilize-a-system-while-it-is-actively-failing-2ph3</guid>
      <description>&lt;h1&gt;
  
  
  Stop diagnosing, start stabilizing: a field guide for live incidents
&lt;/h1&gt;

&lt;p&gt;Your error rate just spiked. Someone is already asking "what changed?" in Slack. Here's the uncomfortable truth: the fastest way to make an incident worse is to start debugging the root cause before you've stopped the bleeding. This is the sequence we use to stabilize production systems first and investigate second, whether that's a single Node server or a full multi-node setup behind a load balancer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two mistakes that make incidents worse
&lt;/h2&gt;

&lt;p&gt;Under pressure, teams almost always do one of these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Change too many things at once, so nobody can tell what fixed (or broke) what&lt;/li&gt;
&lt;li&gt;Start root-causing before the system is even stable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything below exists to prevent those two mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need before an incident happens
&lt;/h2&gt;

&lt;p&gt;You can't stabilize what you can't see or touch. Minimum bar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shell access to affected hosts (SSH or a bastion)&lt;/li&gt;
&lt;li&gt;Basic monitoring: CPU, memory, disk I/O, error rates (Prometheus, Datadog, or even &lt;code&gt;htop&lt;/code&gt; plus logs)&lt;/li&gt;
&lt;li&gt;A load balancer or reverse proxy in front of the app (Nginx, HAProxy, cloud LB)&lt;/li&gt;
&lt;li&gt;A known, tested rollback procedure&lt;/li&gt;
&lt;li&gt;A second person to sanity-check decisions, even just in a Slack thread&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're missing any of this, that's your next project, not something to build mid-incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: freeze all changes
&lt;/h2&gt;

&lt;p&gt;This isn't technical, it's procedural. Post this immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INCIDENT: elevated error rate on checkout-api since 14:32 UTC.
No deploys, no config changes, no manual DB edits until stabilized.
Incident channel: #inc-2024-checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This stops the classic "quick fix" from a well-meaning teammate that stacks a second problem on top of the first one you haven't diagnosed yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: check what changed in the last 24 hours
&lt;/h2&gt;

&lt;p&gt;Most live failures trace back to something recent: a deploy, a config edit, a traffic spike, or an upstream update.&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;# Recent deploys&lt;/span&gt;
git log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"24 hours ago"&lt;/span&gt; &lt;span class="nt"&gt;--oneline&lt;/span&gt;

&lt;span class="c"&gt;# Recent config changes (if infra is version controlled)&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; /etc/nginx log &lt;span class="nt"&gt;--since&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"24 hours ago"&lt;/span&gt;

&lt;span class="c"&gt;# System-level changes&lt;/span&gt;
last &lt;span class="nt"&gt;-x&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /var/log/dpkg.log | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y-%m-%d&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If timing lines up, roll back before you dig further:&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;# Tagged release rollback&lt;/span&gt;
git checkout tags/v2.14.1
./deploy.sh production

&lt;span class="c"&gt;# Kubernetes rollback&lt;/span&gt;
kubectl rollout undo deployment/checkout-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: shed load before you preserve correctness
&lt;/h2&gt;

&lt;p&gt;No obvious recent change, or the rollback didn't help? Reduce load on the failing component. You're trading functionality for stability, temporarily.&lt;/p&gt;

&lt;p&gt;Ordered roughly by disruption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limit at the edge (per IP or per token)&lt;/li&gt;
&lt;li&gt;Kill non-critical endpoints: recommendations, analytics beacons, background reports&lt;/li&gt;
&lt;li&gt;Maintenance page for non-essential routes, keep checkout/login alive&lt;/li&gt;
&lt;li&gt;Scale horizontally if the bottleneck is compute, not a shared resource&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Emergency Nginx rate limit:&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;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=emergency:10m&lt;/span&gt; &lt;span class="s"&gt;rate=5r/s&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;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=emergency&lt;/span&gt; &lt;span class="s"&gt;burst=10&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req_status&lt;/span&gt; &lt;span class="mi"&gt;503&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://backend&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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; nginx &lt;span class="nt"&gt;-s&lt;/span&gt; reload
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: the database goes first
&lt;/h2&gt;

&lt;p&gt;Databases usually fail last and recover slowest. A saturated DB will keep everything else unstable no matter what you fix upstream.&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;-- Connections vs max&lt;/span&gt;
&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="nv"&gt;"SELECT count(*) FROM pg_stat_activity;"&lt;/span&gt;
&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="nv"&gt;"SHOW max_connections;"&lt;/span&gt;

&lt;span class="c1"&gt;-- Long-running queries&lt;/span&gt;
&lt;span class="n"&gt;psql&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt; &lt;span class="nv"&gt;"SELECT pid, now() - query_start AS duration, query
         FROM pg_stat_activity
         WHERE state = 'active'
         ORDER BY duration DESC LIMIT 10;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kill runaway queries holding locks:&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;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_terminate_backend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;14832&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If connection exhaustion is the pattern, PgBouncer fixes this without touching app code. No pooler running? That's your action item once the fire's out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: fail over instead of fighting a bad node
&lt;/h2&gt;

&lt;p&gt;Disk pressure, memory leak, bad kernel state on one box: pull it from rotation instead of debugging it live.&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;# HAProxy&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"disable server backend/web03"&lt;/span&gt; | socat stdio /var/run/haproxy.sock

&lt;span class="c"&gt;# Kubernetes&lt;/span&gt;
kubectl cordon node-3
kubectl drain node-3 &lt;span class="nt"&gt;--ignore-daemonsets&lt;/span&gt; &lt;span class="nt"&gt;--delete-emptydir-data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once it's out of rotation, it's a forensic artifact you can inspect at your own pace, not a liability sitting in the traffic path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: communicate facts, not guesses
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STATUS 15:10 UTC: error rate back to baseline (0.2%) after rolling back
deploy v2.14.2 and draining node-3. Root cause under investigation.
Next update in 30 min or on change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't close the incident the second the dashboard looks calm.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually verify stability
&lt;/h2&gt;

&lt;p&gt;One green graph isn't proof. Hold these steady for 30 to 60 minutes at real traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error rate&lt;/strong&gt;: back to baseline, checked in 5-minute buckets, not instant values&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: p50, p95, and p99 all recovered, a good average can hide a bad p99&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource headroom&lt;/strong&gt;: CPU, memory, connection pools have actual margin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queue depth&lt;/strong&gt;: backlogs draining, not just growing slower
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Nginx status code breakdown&lt;/span&gt;
&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; 5000 /var/log/nginx/access.log | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $9}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;

&lt;span class="c"&gt;# Live connection stats&lt;/span&gt;
watch &lt;span class="nt"&gt;-n&lt;/span&gt; 2 &lt;span class="s2"&gt;"ss -s"&lt;/span&gt;

&lt;span class="c"&gt;# DB connection headroom&lt;/span&gt;
psql &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"SELECT count(*), max_conn FROM pg_stat_activity, (SELECT setting::int AS max_conn FROM pg_settings WHERE name='max_connections') s GROUP BY max_conn;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare current traffic-normalized error rate to the same time last week, not to five minutes ago. Post-load-shedding quiet periods lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfalls to avoid
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Root-causing mid-incident&lt;/strong&gt;: adds risk, rarely speeds recovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rolling back and rolling forward in the same window&lt;/strong&gt;: give one change time to prove itself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Declaring victory too early&lt;/strong&gt;: five calm minutes isn't a stable system&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skipping the change freeze&lt;/strong&gt;: uncoordinated fixes are how you get two incidents instead of one&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stabilize first. Root-cause after. Every time.&lt;/p&gt;

&lt;p&gt;Full original write-up with more context: &lt;a href="https://binadit.com/blog/stabilize-system-active-failure-infrastructure-management-services" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://binadit.com/blog/stabilize-system-active-failure-infrastructure-management-services" rel="noopener noreferrer"&gt;binadit.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <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>
  </channel>
</rss>
