<?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: Twinhull</title>
    <description>The latest articles on DEV Community by Twinhull (@twinhull).</description>
    <link>https://dev.to/twinhull</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%2F4147780%2Fdb67d409-cbb1-426a-a952-4a1fdfc9734a.png</url>
      <title>DEV Community: Twinhull</title>
      <link>https://dev.to/twinhull</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/twinhull"/>
    <language>en</language>
    <item>
      <title>How long does a Patroni failover take? We measured it</title>
      <dc:creator>Twinhull</dc:creator>
      <pubDate>Mon, 05 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/twinhull/how-long-does-a-patroni-failover-take-we-measured-it-4g1i</link>
      <guid>https://dev.to/twinhull/how-long-does-a-patroni-failover-take-we-measured-it-4g1i</guid>
      <description>&lt;p&gt;Tested on: Patroni 4.1.5, etcd 3.6.15, HAProxy 2.8, PostgreSQL 16&lt;/p&gt;

&lt;p&gt;“A few seconds” is the usual answer. It's true for one kind of failover and wrong for the one that matters most. Here are real numbers from a 3-node cluster, what drives them, and what tuning actually buys you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happened to the leader&lt;/th&gt;
&lt;th&gt;Writes unavailable&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Planned switchover (&lt;code&gt;patronictl switchover&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;4.7–5.0 s&lt;/td&gt;
&lt;td&gt;Leader steps down cleanly and hands over the lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean stop (&lt;code&gt;systemctl stop patroni&lt;/code&gt;, reboot)&lt;/td&gt;
&lt;td&gt;4.7–7.0 s&lt;/td&gt;
&lt;td&gt;Patroni releases the lock as it shuts down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hard crash (power loss, kernel panic, &lt;code&gt;kill -9&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;24–30 s&lt;/td&gt;
&lt;td&gt;Nobody releases the lock; it has to expire&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Default timings (ttl 30 s, loop_wait 10 s, retry_timeout 10 s). Asynchronous replication, HAProxy routing on the Patroni REST API. Ranges cover repeated runs. Zero committed transactions were lost in every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we measured
&lt;/h2&gt;

&lt;p&gt;A writer opened a new connection through HAProxy's read-write port and committed one row every 200 ms, the way an application with a retrying connection pool behaves. We then took the leader away and recorded:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write downtime:&lt;/strong&gt; from the first failed write to the first successful write on the new leader. This is what your users feel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lost transactions:&lt;/strong&gt; every row the client saw as committed, checked against the database afterwards.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejoin time:&lt;/strong&gt; until the old leader streamed again as a replica, which shows that &lt;code&gt;pg_rewind&lt;/code&gt; worked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We measured the write path end to end, not just Patroni's log, because what matters is when writes succeed again. That includes Patroni's election, PostgreSQL's promotion, and HAProxy noticing the new leader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a crash takes ~30 seconds
&lt;/h2&gt;

&lt;p&gt;Patroni's leader holds a lock (a key with a time-to-live) in etcd and renews it every &lt;code&gt;loop_wait&lt;/code&gt; seconds. When the leader is stopped or switched over, Patroni deletes the key, so a replica can take over on its next loop.&lt;/p&gt;

&lt;p&gt;When the leader &lt;em&gt;crashes&lt;/em&gt;, nothing deletes the key. The replicas must assume the leader might still be alive, perhaps just slow, until the lock expires after &lt;code&gt;ttl&lt;/code&gt; seconds. That waiting is deliberate: it's what stops two nodes believing they're both the primary. So crash failover time is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ttl  (time since the last renewal, up to 30 s)
+ promotion of the best replica      (~1 s)
+ HAProxy health check notices it    (1–3 s)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the default &lt;code&gt;ttl: 30&lt;/code&gt;, we measured 24 to 30 seconds. The spread comes from when in the renewal cycle the crash happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What tuning buys you
&lt;/h2&gt;

&lt;p&gt;The three settings live in Patroni's cluster configuration (&lt;code&gt;patronictl edit-config&lt;/code&gt;) and must satisfy &lt;code&gt;ttl ≥ loop_wait + 2 × retry_timeout&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ttl / loop_wait / retry_timeout&lt;/th&gt;
&lt;th&gt;Crash downtime measured&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;30 / 10 / 10 (default)&lt;/td&gt;
&lt;td&gt;24–30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20 / 5 / 5&lt;/td&gt;
&lt;td&gt;19.9–20.8 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cutting the timings by a third saved us about 5–8 seconds. It comes at a cost: &lt;code&gt;retry_timeout&lt;/code&gt; is also how long Patroni tolerates a slow etcd or a network blip before it gives up. Set it too low, and a 6-second hiccup on a busy network demotes a healthy primary. You'd trade a rare 30-second outage for more frequent, unnecessary 5-second ones. For most clusters on a normal LAN, the defaults are right.&lt;/p&gt;

&lt;p&gt;Two settings that matter more than people expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;failsafe_mode: true&lt;/code&gt;.&lt;/strong&gt; Without it, losing etcd quorum demotes the primary even though PostgreSQL is fine. In our test, with 2 of 3 etcd members down and failsafe mode on, the primary kept accepting writes for the whole test. Only automatic failover was lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HAProxy's check interval.&lt;/strong&gt; &lt;code&gt;inter 2s fastinter 500ms fall 2 rise 1&lt;/code&gt; detects the change within a couple of seconds. The common tutorial value &lt;code&gt;inter 3s fall 3 rise 2&lt;/code&gt; can add 6–9 seconds on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Does synchronous replication slow failover down?
&lt;/h2&gt;

&lt;p&gt;Not measurably. With &lt;code&gt;synchronous_mode: true&lt;/code&gt; a crash took 23.2 seconds, within the async range. What synchronous mode changes is what you can &lt;em&gt;lose&lt;/em&gt;: it guarantees zero lost committed transactions, at the cost of one network round trip on every commit. Async lost nothing in our runs too, but it can't promise that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a switchover still takes ~5 seconds
&lt;/h2&gt;

&lt;p&gt;Even a planned switchover interrupts writes: the old leader must stop accepting writes, flush its last WAL to the candidate, and restart as a replica, while the candidate is promoted. Existing connections are cut. Your application needs to reconnect. That's why a connection pool with automatic reconnect matters as much as anything in the database layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure your own cluster
&lt;/h2&gt;

&lt;p&gt;Your network, disks and HAProxy placement will produce different numbers. Measure before you promise an RTO to anyone:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Build a staging cluster that matches production.&lt;/li&gt;
&lt;li&gt; Run a writer through the same connection path your applications use.&lt;/li&gt;
&lt;li&gt; Test all three: switchover, clean stop, and a real crash (&lt;code&gt;SIGKILL&lt;/code&gt; to Patroni &lt;em&gt;and&lt;/em&gt; PostgreSQL).&lt;/li&gt;
&lt;li&gt; Check lost rows, not just downtime. Confirm the old leader rejoins by itself.&lt;/li&gt;
&lt;li&gt; Repeat each test several times and report the range, not the best run.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://twinhullhq.com/articles/how-long-does-patroni-failover-take.html" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;. The health-check script used in these tests is free and open source: &lt;a href="https://github.com/twinhullhq/ha-check" rel="noopener noreferrer"&gt;github.com/twinhullhq/ha-check&lt;/a&gt;. If you run Patroni in production, the full kit (templates, failover drills, DR runbook, 3-node lab) is at &lt;a href="https://twinhullhq.com" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
      <category>sre</category>
    </item>
    <item>
      <title>Patroni: pg_rewind “password authentication failed” and the PGPASSWORD trap</title>
      <dc:creator>Twinhull</dc:creator>
      <pubDate>Sat, 03 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/twinhull/patroni-pgrewind-password-authentication-failed-and-the-pgpassword-trap-1nai</link>
      <guid>https://dev.to/twinhull/patroni-pgrewind-password-authentication-failed-and-the-pgpassword-trap-1nai</guid>
      <description>&lt;p&gt;Tested on: Patroni 4.1, PostgreSQL 16&lt;/p&gt;

&lt;p&gt;Your cluster passes every switchover test. Then the leader crashes for real, a replica takes over as it should, and the old leader never comes back: it sits in &lt;code&gt;start failed&lt;/code&gt;. We hit this in our own test lab. This article covers the cause, a one-line check, and the fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;patronictl list&lt;/code&gt; shows the crashed node stuck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| pg2    | 10.0.0.12:5432 | Replica | start failed |    |     unknown |     |    unknown |     |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PostgreSQL's log on that node repeats every few seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FATAL:  requested timeline 3 is not a child of this server's history
DETAIL:  Latest checkpoint is at 0/8000028 on timeline 2, but in the history of the
         requested timeline, the server forked off from that timeline at 0/7008BA0.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message only says the old leader wrote WAL the new leader never saw, so it can't follow the new timeline. Patroni is supposed to fix exactly this with &lt;code&gt;pg_rewind&lt;/code&gt;. The Patroni log shows why it didn't:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO: running pg_rewind from pg3
INFO: running pg_rewind from dbname=postgres user=rewind_user host=10.0.0.13 port=5432 ...
INFO: pg_rewind exit code=1
INFO:  stderr=pg_rewind: error: connection to server at "10.0.0.13", port 5432 failed:
       FATAL:  password authentication failed for user "rewind_user"
ERROR: Failed to rewind from healthy primary: pg3
INFO: starting as a secondary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The cause: PGPASSWORD beats .pgpass
&lt;/h2&gt;

&lt;p&gt;Patroni doesn't pass passwords on the command line. It writes them to a &lt;code&gt;.pgpass&lt;/code&gt; file (the &lt;code&gt;postgresql.pgpass&lt;/code&gt; setting) and points &lt;code&gt;pg_rewind&lt;/code&gt; and &lt;code&gt;pg_basebackup&lt;/code&gt; at it. But libpq, the library under every PostgreSQL client, looks for a password in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; a password in the connection string itself,&lt;/li&gt;
&lt;li&gt; the &lt;strong&gt;&lt;code&gt;PGPASSWORD&lt;/code&gt; environment variable&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt; the password file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So if Patroni was started with &lt;code&gt;PGPASSWORD&lt;/code&gt; in its environment, typically the superuser's password left over from someone's shell, &lt;code&gt;pg_rewind&lt;/code&gt; sends &lt;em&gt;that&lt;/em&gt; password as &lt;code&gt;rewind_user&lt;/code&gt;. Authentication fails, and the rewind is skipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;The variable only hurts the Patroni process that inherited it. Nodes started cleanly by systemd weeks ago keep replicating happily. The dangerous moment is &lt;strong&gt;the restart after an incident&lt;/strong&gt;: the leader crashed, someone logs in, runs a few &lt;code&gt;psql&lt;/code&gt; commands with &lt;code&gt;PGPASSWORD&lt;/code&gt; exported, and starts Patroni from that same shell, or a recovery script does it for them. That process is the one that has to run &lt;code&gt;pg_rewind&lt;/code&gt;, and it's the one carrying the wrong password.&lt;/p&gt;

&lt;p&gt;It gets worse: Patroni points replication at the same &lt;code&gt;.pgpass&lt;/code&gt; file (&lt;code&gt;primary_conninfo&lt;/code&gt; uses &lt;code&gt;passfile=…&lt;/code&gt;, not a password), so even after a manual rebuild, a node running with that environment can't stream from the leader either. Planned switchovers in a clean test environment never show any of this. In our lab, switchovers passed every time, while every crash test failed until we found the variable in the restart path.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the variable gets there
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Starting Patroni by hand&lt;/strong&gt; after an incident, from a shell where you exported &lt;code&gt;PGPASSWORD&lt;/code&gt; to run &lt;code&gt;psql&lt;/code&gt;, e.g. &lt;code&gt;nohup patroni patroni.yml &amp;amp;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapper and deploy scripts&lt;/strong&gt; that &lt;code&gt;source&lt;/code&gt; an environment file with &lt;code&gt;set -a&lt;/code&gt;, which exports every variable in it, and then start services.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;systemd units&lt;/strong&gt; with &lt;code&gt;Environment=PGPASSWORD=…&lt;/code&gt; or an &lt;code&gt;EnvironmentFile=&lt;/code&gt; shared with other tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container images&lt;/strong&gt; that set &lt;code&gt;PGPASSWORD&lt;/code&gt; for convenience.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Check in 10 seconds
&lt;/h2&gt;

&lt;p&gt;Look at the environment of the running Patroni process on every node:&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 tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &amp;lt; /proc/&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'bin/patroni'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/environ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'^PG|^PATRONI_'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any &lt;code&gt;PGPASSWORD&lt;/code&gt;, &lt;code&gt;PGUSER&lt;/code&gt;, &lt;code&gt;PGHOST&lt;/code&gt; or &lt;code&gt;PGSERVICE&lt;/code&gt; line is a problem. So is any unexpected &lt;code&gt;PATRONI_*&lt;/code&gt; line: Patroni reads every &lt;code&gt;PATRONI_&lt;/code&gt;-prefixed variable as configuration, and it overrides &lt;code&gt;patroni.yml&lt;/code&gt;. That's a related trap we also hit, with a &lt;code&gt;PATRONI_LOG_DIR&lt;/code&gt; from a script sending logs somewhere unexpected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; Start Patroni only through systemd, with a clean environment:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;    &lt;span class="k"&gt;[Service]&lt;/span&gt;
    &lt;span class="nt"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;postgres
    &lt;span class="nt"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;PATH=/opt/patroni/bin:/usr/local/bin:/usr/bin:/bin
    &lt;span class="nt"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;/opt/patroni/bin/patroni /etc/patroni/patroni.yml
    &lt;span class="nt"&gt;KillMode&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Remove any &lt;code&gt;PGPASSWORD&lt;/code&gt; from &lt;code&gt;Environment=&lt;/code&gt; and &lt;code&gt;EnvironmentFile=&lt;/code&gt; lines, then &lt;code&gt;systemctl daemon-reload &amp;amp;&amp;amp; systemctl restart patroni&lt;/code&gt;. PostgreSQL keeps running while Patroni restarts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;On the stuck node, restarting Patroni is usually enough: it retries &lt;code&gt;pg_rewind&lt;/code&gt; with the right password. If the rewind can't work any more (the WAL it needs has been recycled), rebuild it: &lt;code&gt;patronictl reinit &amp;lt;cluster&amp;gt; &amp;lt;node&amp;gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Prove it's fixed
&lt;/h2&gt;

&lt;p&gt;A switchover won't prove anything here; only a crash will. In a test environment, kill the leader's Patroni and PostgreSQL with &lt;code&gt;SIGKILL&lt;/code&gt; while writing, wait for the new leader, restart the old node, and confirm it comes back as a streaming replica. That's exactly what our &lt;code&gt;failover-drill --mode crash&lt;/code&gt; automates. After the fix, our lab's crashed leaders rejoined in about 34 seconds, every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  This trap is handled in the Twinhull HA Kit
&lt;/h3&gt;

&lt;p&gt;The kit's systemd units start Patroni with a clean environment, &lt;code&gt;preflight&lt;/code&gt; warns about exported &lt;code&gt;PG*&lt;/code&gt; variables, and the config generator refuses &lt;code&gt;PATRONI_*&lt;/code&gt; names. The crash drill proves the rewind works before production needs it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twinhullhq.com/index.html" rel="noopener noreferrer"&gt;See the kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://twinhullhq.com/articles/patroni-pg-rewind-password-authentication-failed.html" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;. The health-check script used in these tests is free and open source: &lt;a href="https://github.com/twinhullhq/ha-check" rel="noopener noreferrer"&gt;github.com/twinhullhq/ha-check&lt;/a&gt;. If you run Patroni in production, the full kit (templates, failover drills, DR runbook, 3-node lab) is at &lt;a href="https://twinhullhq.com" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>devops</category>
      <category>database</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Patroni “Failed to get list of machines… 'int' object has no attribute 'get'” with etcd on Ubuntu 24.04</title>
      <dc:creator>Twinhull</dc:creator>
      <pubDate>Thu, 01 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/twinhull/patroni-failed-to-get-list-of-machines-int-object-has-no-attribute-get-with-etcd-on-ubuntu-4ie1</link>
      <guid>https://dev.to/twinhull/patroni-failed-to-get-list-of-machines-int-object-has-no-attribute-get-with-etcd-on-ubuntu-4ie1</guid>
      <description>&lt;p&gt;Tested on: Patroni 4.1.5, etcd 3.4.30 → 3.6.15&lt;/p&gt;

&lt;p&gt;You installed etcd with &lt;code&gt;apt&lt;/code&gt;, started a healthy 3-member cluster, and pointed Patroni at it. Patroni never starts PostgreSQL. It just logs the same error every five seconds. The problem is the etcd version, not your configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Failed to get list of machines from http://127.0.0.1:2379/v3: AttributeError("'int' object has no attribute 'get'")
ERROR: Failed to get list of machines from http://127.0.0.1:2380/v3: AttributeError("'int' object has no attribute 'get'")
INFO: waiting on etcd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile etcd itself looks perfectly healthy, which is what makes this confusing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ etcdctl endpoint health
127.0.0.1:2379 is healthy: successfully committed proposal: took = 2.1ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;Patroni's &lt;code&gt;etcd3&lt;/code&gt; backend doesn't use gRPC. It talks to etcd's &lt;strong&gt;JSON gateway&lt;/strong&gt; over plain HTTP, the &lt;code&gt;/v3/…&lt;/code&gt; endpoints. Ubuntu 24.04's &lt;code&gt;etcd-server&lt;/code&gt; package is etcd &lt;strong&gt;3.4.30&lt;/strong&gt;. On that build, the endpoint Patroni calls first to discover cluster members doesn't answer the way Patroni expects:&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="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://127.0.0.1:2379/version
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"etcdserver"&lt;/span&gt;:&lt;span class="s2"&gt;"3.4.30"&lt;/span&gt;,&lt;span class="s2"&gt;"etcdcluster"&lt;/span&gt;:&lt;span class="s2"&gt;"3.4.0"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://127.0.0.1:2379/v3/cluster/member/list &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt;
404 page not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Patroni gets back something that isn't the JSON object it expects, fails while parsing it, and retries forever. The error names a Python type, not etcd, which sends people looking in the wrong place.&lt;/p&gt;

&lt;p&gt;The same request against etcd 3.6 returns the member list, and Patroni bootstraps immediately. We confirmed both on the same machine while testing the Twinhull kit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Confirm it on your system
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;etcd &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;                 &lt;span class="c"&gt;# 3.4.x → this is your problem&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://&amp;lt;etcd-ip&amp;gt;:2379/v3/cluster/member/list &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the second command prints &lt;code&gt;404 page not found&lt;/code&gt; instead of JSON with a &lt;code&gt;members&lt;/code&gt; array, upgrade etcd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: install etcd 3.5 or newer from the official release
&lt;/h2&gt;

&lt;p&gt;Distribution packages lag behind. Install the upstream release binaries, which are statically linked and have no dependencies (current as of September 2026: 3.6.15; check &lt;a href="https://github.com/etcd-io/etcd/releases" rel="noopener noreferrer"&gt;github.com/etcd-io/etcd/releases&lt;/a&gt;):&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;systemctl disable &lt;span class="nt"&gt;--now&lt;/span&gt; etcd                    &lt;span class="c"&gt;# stop the distro etcd&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt remove &lt;span class="nt"&gt;-y&lt;/span&gt; etcd-server etcd-client

&lt;span class="nv"&gt;ETCD_VER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v3.6.15
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/etcd.tgz &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/etcd-io/etcd/releases/download/&lt;span class="nv"&gt;$ETCD_VER&lt;/span&gt;/etcd-&lt;span class="nv"&gt;$ETCD_VER&lt;/span&gt;&lt;span class="nt"&gt;-linux-amd64&lt;/span&gt;.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf /tmp/etcd.tgz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 /tmp/etcd-&lt;span class="nv"&gt;$ETCD_VER&lt;/span&gt;&lt;span class="nt"&gt;-linux-amd64&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;etcd,etcdctl,etcdutl&lt;span class="o"&gt;}&lt;/span&gt; /usr/local/bin/
etcd &lt;span class="nt"&gt;--version&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;                             &lt;span class="c"&gt;# etcd Version: 3.6.15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run etcd from &lt;code&gt;/usr/local/bin/etcd&lt;/code&gt; with your own systemd unit and a config file (&lt;code&gt;--config-file /etc/etcd/etcd.conf.yml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upgrading an existing 3.4 cluster?&lt;/strong&gt; etcd supports upgrading one minor version at a time: 3.4 → 3.5 → 3.6, member by member. For a new Patroni cluster that has never stored anything, it's simpler to stop all members, wipe their data directories, and start fresh on 3.6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two more things while you're here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't switch Patroni to the old &lt;code&gt;etcd&lt;/code&gt; (v2) backend&lt;/strong&gt; to work around this. The v2 API is deprecated, and etcd 3.6 no longer serves it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;etcdctl&lt;/code&gt; without a proxy.&lt;/strong&gt; If your servers export &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, add the etcd addresses to &lt;code&gt;NO_PROXY&lt;/code&gt;, or health checks may go through the proxy and time out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Caught automatically in the Twinhull HA Kit
&lt;/h3&gt;

&lt;p&gt;The kit's &lt;code&gt;preflight&lt;/code&gt; check fails on etcd versions older than 3.5 before you install anything, and the guide installs the official 3.6 release with a hardened systemd unit and optional mutual TLS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twinhullhq.com/index.html" rel="noopener noreferrer"&gt;See the kit&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://twinhullhq.com/articles/patroni-etcd-attributeerror-int-object-has-no-attribute-get.html" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;. The health-check script used in these tests is free and open source: &lt;a href="https://github.com/twinhullhq/ha-check" rel="noopener noreferrer"&gt;github.com/twinhullhq/ha-check&lt;/a&gt;. If you run Patroni in production, the full kit (templates, failover drills, DR runbook, 3-node lab) is at &lt;a href="https://twinhullhq.com" rel="noopener noreferrer"&gt;twinhullhq.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ubuntu</category>
      <category>devops</category>
      <category>database</category>
    </item>
  </channel>
</rss>
