<?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: Leonardo Bagno</title>
    <description>The latest articles on DEV Community by Leonardo Bagno (@leobagnolabs).</description>
    <link>https://dev.to/leobagnolabs</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%2F1373513%2F84a2eecd-1a4b-4b68-8d18-9d348160e169.png</url>
      <title>DEV Community: Leonardo Bagno</title>
      <link>https://dev.to/leobagnolabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/leobagnolabs"/>
    <language>en</language>
    <item>
      <title>I Stopped My PostgreSQL Primary 10 Times to Measure Real RPO</title>
      <dc:creator>Leonardo Bagno</dc:creator>
      <pubDate>Sun, 19 Jul 2026 12:10:22 +0000</pubDate>
      <link>https://dev.to/leobagnolabs/i-stopped-my-postgresql-primary-10-times-to-measure-real-rpo-218p</link>
      <guid>https://dev.to/leobagnolabs/i-stopped-my-postgresql-primary-10-times-to-measure-real-rpo-218p</guid>
      <description>&lt;p&gt;In my previous article, I used real measurements to show that lowering the DNS TTL does not necessarily make Route 53 failover faster. The lesson was simple: test the system instead of assuming how it behaves.&lt;/p&gt;

&lt;p&gt;This time, I wanted to measure RPO.&lt;/p&gt;

&lt;p&gt;RPO, or Recovery Point Objective, describes how much data may be lost when a system fails. Disaster recovery projects often claim an RPO of "near zero," but that number is not always tested.&lt;/p&gt;

&lt;p&gt;My dissertation project, &lt;code&gt;leomulticloud&lt;/code&gt;, originally had an RPO of zero because the application was stateless. In version 5.0, I am adding PostgreSQL logical replication between AWS and Azure. This raised a practical question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the primary stops while receiving writes, how many confirmed rows will be missing from the standby?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To begin answering it, I stopped my local PostgreSQL primary ten times.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;I built the first version of the lab locally using Docker Compose inside WSL2.&lt;/p&gt;

&lt;p&gt;It used two PostgreSQL 18 containers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pg-primary&lt;/code&gt;, representing the AWS database&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg-standby&lt;/code&gt;, representing the Azure database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They were connected through native PostgreSQL logical replication using a publication and subscription.&lt;/p&gt;

&lt;p&gt;I also wrote a Python script called &lt;code&gt;measure_rpo.py&lt;/code&gt; using &lt;code&gt;psycopg&lt;/code&gt;. The script continuously inserted timestamped rows into the primary. It used a synchronous, single-threaded writer, so it waited for each insert to be confirmed before sending the next one.&lt;/p&gt;

&lt;p&gt;During each run, I stopped the primary from another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop pg-primary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the primary became unavailable, the script compared the last ID confirmed by the primary with the highest ID present on the standby. The difference was the measured RPO.&lt;/p&gt;

&lt;p&gt;I tested nominal rates of 10, 100, and 1,000 writes per second. Including an earlier baseline run, I performed ten runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Battery&lt;/th&gt;
&lt;th&gt;Nominal rate&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;RPO in rows&lt;/th&gt;
&lt;th&gt;RPO in seconds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;10/s&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A&lt;/td&gt;
&lt;td&gt;10/s&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0, 0, 0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;100/s&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0, 0, 0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C&lt;/td&gt;
&lt;td&gt;1,000/s&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;0, 0, 0&lt;/td&gt;
&lt;td&gt;0.0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All ten runs produced a measured RPO of zero. Every row confirmed by the primary was present on the standby when I checked it.&lt;/p&gt;

&lt;p&gt;However, this does not mean that PostgreSQL logical replication guarantees zero data loss. It means that the standby kept up with this particular workload in my local environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The writer reached its limit first
&lt;/h2&gt;

&lt;p&gt;The most useful finding came from the tests configured for 1,000 writes per second.&lt;/p&gt;

&lt;p&gt;The actual rate was only around 330 to 345 writes per second, based on the timestamps stored in the database. My estimate while watching the test was much higher, but the recorded data corrected it.&lt;/p&gt;

&lt;p&gt;The bottleneck was the test script, not the replication.&lt;/p&gt;

&lt;p&gt;Because the writer waited for each insert to be confirmed, every cycle took approximately three milliseconds. Reaching 1,000 writes per second would require each cycle to finish in approximately one millisecond.&lt;/p&gt;

&lt;p&gt;I therefore reached the limit of my writer before reaching the limit of the replication system.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this experiment does not prove
&lt;/h2&gt;

&lt;p&gt;This was a local baseline, not a test of full cross-cloud conditions. Replication used Docker networking, and the workload came from one synchronous writer that peaked at approximately 345 writes per second. The results therefore apply only to this environment and workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Across ten local runs, the measured RPO was zero. Under this specific workload, the subscriber contained every row confirmed by the primary before it stopped.&lt;/p&gt;

&lt;p&gt;The next step is to repeat the experiment across AWS and Azure, with a real network between the publisher and subscriber. This should provide a more realistic measurement of cross-cloud replication RPO. The findings should come out soon.&lt;/p&gt;

&lt;p&gt;The code, Docker Compose configuration, and CSV files from all ten runs are available in the &lt;a href="https://github.com/leobagno-labs/leomulticloud" rel="noopener noreferrer"&gt;leomulticloud repository&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Lowering your DNS TTL will not speed up your failover, and here is the data showing why</title>
      <dc:creator>Leonardo Bagno</dc:creator>
      <pubDate>Sun, 12 Jul 2026 10:18:28 +0000</pubDate>
      <link>https://dev.to/leobagnolabs/lowering-your-dns-ttl-will-not-speed-up-your-failover-and-here-is-the-data-showing-why-32m3</link>
      <guid>https://dev.to/leobagnolabs/lowering-your-dns-ttl-will-not-speed-up-your-failover-and-here-is-the-data-showing-why-32m3</guid>
      <description>&lt;p&gt;&lt;em&gt;I spent three months measuring multi-cloud DNS failover for my final-year dissertation. The most useful thing I learned contradicts common advice, so I'm sharing the numbers.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumption
&lt;/h2&gt;

&lt;p&gt;When the AWS US-East-1 outage hit in October 2025, half the internet went down with it. The standard answer to "how do I survive a cloud outage?" is DNS-based failover: run a standby in another cloud, point a Route 53 health check at your primary, and let DNS redirect traffic when it fails.&lt;/p&gt;

&lt;p&gt;And the standard tuning advice that comes with it: &lt;strong&gt;lower your TTL to fail over faster.&lt;/strong&gt; Set it to 60 seconds instead of 300 and you'll recover five times quicker. It sounds obvious. It's also wrong. Or at least, it's only a small part of the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The experiment
&lt;/h2&gt;

&lt;p&gt;I built a deliberately minimal setup so the measurements would be clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A stateless Flask weather app deployed identically on AWS EC2 (eu-west-1) and an Azure VM (northeurope), via Terraform&lt;/li&gt;
&lt;li&gt;Route 53 with failover routing and a health check on the primary&lt;/li&gt;
&lt;li&gt;No Docker, no orchestration. I removed anything that added boot-time noise to the measurements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I killed the primary. Nine times, across three TTL configurations (60s, 120s, 300s), measuring from multiple DNS resolvers, plus three failback runs in the other direction. Every run logged to CSV.&lt;/p&gt;

&lt;h2&gt;
  
  
  The finding
&lt;/h2&gt;

&lt;p&gt;Failover time (RTO) has two separate components, and most advice mixes them up:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Detection time: how long Route 53 takes to notice your primary is dead.&lt;/strong&gt; In my runs this came out at ~48 seconds, and here's the key part: it was &lt;strong&gt;constant across every TTL configuration&lt;/strong&gt;. TTL=60 and TTL=300 detected the failure in the same time, because detection is governed by Route 53's internal polling and quorum logic, not by the TTL advertised to resolvers. To reduce it, you need to adjust the health check interval and the failure threshold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Propagation time: how long resolvers take to pick up the new record.&lt;/strong&gt; This is where TTL matters, but it turned out to be resolver-dependent, and sometimes dramatically so. The clearest example: at TTL=300s, Cloudflare's resolver kept oscillating for over 325 seconds after the failover, while Google DNS stabilised much faster. Same record, same TTL, very different behaviour depending on who resolves your users' queries.&lt;/p&gt;

&lt;p&gt;The full numbers:&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;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean failover RTO (all TTL configs, n=9)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;48.0 s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTO at TTL=60s (mean ± SD)&lt;/td&gt;
&lt;td&gt;46.3 s ± 0.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTO at TTL=120s (mean ± SD)&lt;/td&gt;
&lt;td&gt;45.3 s ± 2.9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTO at TTL=300s (mean ± SD)&lt;/td&gt;
&lt;td&gt;52.3 s ± 7.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failback RTO at TTL=60s (mean ± SD)&lt;/td&gt;
&lt;td&gt;35.3 s ± 3.1 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route 53 detection time&lt;/td&gt;
&lt;td&gt;~48 s, TTL-independent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RPO&lt;/td&gt;
&lt;td&gt;0 s (stateless workload)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total infrastructure cost&lt;/td&gt;
&lt;td&gt;&amp;lt; €5 across all three iterations&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Notice the pattern: TTL=60 and TTL=120 land within noise of each other, because detection dominates both. Only at TTL=300 does the RTO climb, and its standard deviation more than doubles, because now you're at the mercy of resolver caching. RPO was zero, but only because the app was stateless. A real application with a database would not get the same result.&lt;/p&gt;

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

&lt;p&gt;If your failover is taking 60+ seconds and you respond by dropping the TTL from 300 to 60, you'll be disappointed. You're optimising the small, variable component while the large, constant one (health check detection) stays exactly where it was.&lt;/p&gt;

&lt;p&gt;The practical order of operations is the opposite of the common advice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tune the health check first.&lt;/strong&gt; Interval and failure threshold set the floor for your RTO. This is the big lever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then lower the TTL.&lt;/strong&gt; It helps with the tail, not the floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;And don't trust the TTL blindly&lt;/strong&gt;, because resolvers between you and your users have opinions of their own.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Reproduce it for under €5
&lt;/h2&gt;

&lt;p&gt;Everything is public and runs on free-tier-sized instances: the Terraform code, the measurement scripts, and the raw CSVs from every run.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/leobagno-labs/leomulticloud" rel="noopener noreferrer"&gt;https://github.com/leobagno-labs/leomulticloud&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Archived release with DOI: &lt;a href="https://doi.org/10.5281/zenodo.20189820" rel="noopener noreferrer"&gt;https://doi.org/10.5281/zenodo.20189820&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total cloud spend for the entire experimental campaign was under €5. If you want to verify my numbers or run the tests against a different DNS provider, &lt;code&gt;terraform apply&lt;/code&gt; gets you there.&lt;/p&gt;

&lt;h2&gt;
  
  
  New findings coming soon
&lt;/h2&gt;

&lt;p&gt;The stateless app made RPO trivially zero, which avoids the hardest part of real disaster recovery: data. So this project is not finished. I'm already working on the next iteration, and the numbers will land here and in the repo.&lt;/p&gt;

&lt;p&gt;Stay tuned and follow the repo for more to come.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>azure</category>
      <category>dns</category>
    </item>
  </channel>
</rss>
