{"id":45342,"date":"2026-06-26T21:11:41","date_gmt":"2026-06-26T19:11:41","guid":{"rendered":"https:\/\/www.dbi-services.com\/blog\/?p=45342"},"modified":"2026-06-26T21:11:43","modified_gmt":"2026-06-26T19:11:43","slug":"highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived","status":"publish","type":"post","link":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/","title":{"rendered":"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread across replicas. HAProxy handles that routing, with a floating IP from Keepalived in front of it.<\/p>\n\n\n\n<h1 id=\"h-how-the-tools-work-together\" class=\"wp-block-heading\">How the tools work together<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Three components stands between your application and the database.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Patroni<\/strong> manages replication and failover, and it runs an agent on every PostgreSQL node. Each agent exposes a small REST API (port 8008 by default) that reports that node&#8217;s role.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>HAProxy<\/strong> accepts client connections and forwards them to the right node. It asks Patroni&#8217;s REST API which node is the primary and which are replicas, then sends each connection to a matching node.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Keepalived<\/strong> publishes a virtual IP that floats between your HAProxy hosts using VRRP. Your application connects to the VIP, so one HAProxy host going down doesn&#8217;t take the whole entry point with it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your application talks to the VIP. Keepalived points the VIP at a live HAProxy. HAProxy forwards the connection to whichever PostgreSQL node Patroni reports as healthy for that role.<\/p>\n\n\n\n<h1 id=\"h-the-health-check-method\" class=\"wp-block-heading\">The health-check method<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">HAProxy checks one port and routes to another.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Patroni&#8217;s REST API returns an HTTP status that depends on the node&#8217;s role:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>GET \/<\/code> returns <code>200<\/code> only on the leader (the primary). A non-leader node returns <code>503<\/code>.<\/li>\n\n\n\n<li><code>GET \/primary<\/code> is the explicit name for the same leader check.<\/li>\n\n\n\n<li><code>GET \/replica<\/code> returns <code>200<\/code> only on a running replica.<\/li>\n\n\n\n<li><code>GET \/read-only<\/code> returns <code>200<\/code> on the primary or a replica, any node that can serve a read.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">In our case, HAProxy runs its health check against the API port (8008) and reads that status code, then forwards the SQL connection to the database port (5432). A node receives traffic only when its API answers <code>200<\/code> for the role that listener cares about. Point a listener&#8217;s check at <code>\/<\/code> and it follows the primary. Point it at <code>\/replica<\/code> and it follows the replicas. Patroni promotes a new leader, the status codes change, and HAProxy moves traffic to match within a couple of health-check cycles.<\/p>\n\n\n\n<h1 id=\"h-a-first-and-simple-working-configuration\" class=\"wp-block-heading\">A first and simple working configuration<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">A two-node setup with <code>10.5.5.147<\/code> and <code>10.5.5.148<\/code> looks like this. One listener handles writes, the other handles reads.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nlisten PG1\n    bind *:5000\n    option httpchk\n    http-check expect status 200\n    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions\n    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008\n    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008\n\nlisten PG1_ro\n    bind *:5001\n    option httpchk GET \/replica\n    http-check expect status 200\n    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions\n    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008\n    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">This example runs PostgreSQL on port 5432 and the Patroni API on 8008, so swap in whatever ports your deployment uses (the defaults are 5432 and 8008).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Line by line:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>bind *:5000<\/code> and <code>bind *:5001<\/code> are the two addresses your applications connect to. Send writes to 5000 and reads to 5001.<\/li>\n\n\n\n<li><code>option httpchk<\/code> (with no path) on the first listener checks Patroni&#8217;s root endpoint. Only the leader answers <code>200<\/code>, so HAProxy sends port 5000 traffic to the current primary.<\/li>\n\n\n\n<li><code>option httpchk GET \/replica<\/code> on the second listener checks the replica endpoint, so HAProxy sends port 5001 traffic to a replica.<\/li>\n\n\n\n<li><code>http-check expect status 200<\/code> tells HAProxy that <code>200<\/code> means healthy and anything else means down.<\/li>\n\n\n\n<li><code>inter 3s fall 3 rise 2<\/code> checks every 3 seconds, marks a server down after 3 failures, and brings it back after 2 successes.<\/li>\n\n\n\n<li><code>on-marked-down shutdown-sessions<\/code> kills existing connections to a server the instant HAProxy marks it down, so clients reconnect and get rerouted instead of hanging on a dead node.<\/li>\n\n\n\n<li><code>check port 8008<\/code> is the trick in action: health checks hit the Patroni API on 8008 while HAProxy forwards traffic to PostgreSQL on 5432.<\/li>\n\n\n\n<li><code>maxconn 100<\/code> limit connections per server so you don&#8217;t exhaust PostgreSQL&#8217;s connection slots.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For a primary plus one or more replicas, this routes writes and reads to the right node and survives a failover.<\/p>\n\n\n\n<h1 id=\"h-the-failure-mode-hiding-in-the-read-path\" class=\"wp-block-heading\">The failure mode hiding in the read path<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Imagine a two-node cluster: one primary, one replica. The replica goes down. Maybe it crashed, maybe Patroni is mid-switchover and no standby exists for a few seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Your read traffic hits port 5001. That listener marks a server up only when <code>GET \/replica<\/code> returns <code>200<\/code>, and right now no node is a replica. HAProxy has zero usable servers in the pool, so it refuses the connection. Read queries start failing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The primary is up the entire time, and it can serve those reads. Your config won&#8217;t send them there, because you told the read listener to look for replicas and nothing else. You&#8217;ve turned a degraded cluster that could still serve reads into a read outage. You feel this most on small clusters, and each failover passes through a window where the old primary becomes a replica and no standby is available yet. In the worst case, your replica is down, and one of your application is connecting to port 5001, resulting in errors.<\/p>\n\n\n\n<h1 id=\"h-the-fix-fall-back-to-the-primary\" class=\"wp-block-heading\">The fix: fall back to the primary<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Send reads to the primary when the read listener runs out of replicas, instead of dropping them.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nlisten PG1\n    bind *:5000\n    option httpchk\n    http-check expect status 200\n    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions\n    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008\n    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008\n\nlisten PG1_ro\n    bind *:5001\n    option httpchk GET \/replica\n    http-check expect status 200\n    use_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }\n    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions\n    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008\n    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008\n\nbackend PG1_ro_leader\n    option httpchk GET \/primary\n    http-check expect status 200\n    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions\n    server postgresql_10.5.5.147_5432 10.5.5.147:5432 maxconn 100 check port 8008\n    server postgresql_10.5.5.148_5432 10.5.5.148:5432 maxconn 100 check port 8008\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">This new line carries the whole fix:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nuse_backend PG1_ro_leader if { nbsrv(PG1_ro) eq 0 }\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\"><code>nbsrv(PG1_ro)<\/code> counts the usable servers in the <code>PG1_ro<\/code> pool, which here means the number of available replicas, since those servers pass the check only when <code>GET \/replica<\/code> returns <code>200<\/code>. While at least one replica is up, the count stays above zero, the condition is false, and reads stay on the replicas. The moment the last replica drops, <code>nbsrv(PG1_ro)<\/code> hits zero, the condition fires, and HAProxy diverts reads to the <code>PG1_ro_leader<\/code> backend.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">That backend health-checks with <code>GET \/primary<\/code>, so the only server it counts as up is the current primary. HAProxy sends reads to the primary until a replica returns, then shifts them back to the replica pool once a replica passes its check again.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Three names have to agree for this to work. The backend you define (<code>backend PG1_ro_leader<\/code>), the backend you route to (<code>use_backend PG1_ro_leader<\/code>), and the pool you count (<code>nbsrv(PG1_ro)<\/code>) all reference the real section names. Drop in a stale name from an earlier version and HAProxy either refuses to start or counts the wrong pool.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The \/read-only shortcut and what it costs<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Patroni offers <code>GET \/read-only<\/code>, which returns <code>200<\/code> on the primary and the replicas alike. Point the read listener there and both the primary and the replicas serve reads, no fallback backend needed.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The cost is read load on the primary even when your replicas are healthy and idle. The fallback approach keeps reads off the primary until the replicas are gone, then leans on it as a safety net. You protect the primary&#8217;s write capacity during normal operation and still keep reads alive during a replica outage.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To keep lagging replicas out of the read pool, Patroni accepts a threshold on the replica check, for example <code>GET \/replica?lag=10MB<\/code>, which fails any replica more than 10 MB behind. Pair that with the fallback and HAProxy drops the lagging replicas from rotation while reads still have somewhere to go.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Keepalived: removing HAProxy as a single point of failure<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">One HAProxy host fronting the cluster moves the single point of failure up a layer. Run HAProxy on two hosts and let Keepalived float a virtual IP between them with VRRP. Your application connects to the VIP, and whichever HAProxy holds it answers.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A minimal <code>keepalived.conf<\/code> on the primary HAProxy host:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: bash; title: ; notranslate\" title=\"\">\nvrrp_script chk_haproxy {\n    script &quot;killall -0 haproxy&quot;   # succeeds while the haproxy process is alive\n    interval 2\n    weight 2\n}\n\nvrrp_instance VI_1 {\n    interface eth0\n    state MASTER\n    virtual_router_id 51\n    priority 101\n    advert_int 1\n    authentication {\n        auth_type PASS\n        auth_pass changeme\n    }\n    virtual_ipaddress {\n        10.0.0.1\n    }\n    track_script {\n        chk_haproxy\n    }\n}\n<\/pre><\/div>\n\n\n<p class=\"wp-block-paragraph\">The second HAProxy host runs the same file with <code>state BACKUP<\/code> and a lower <code>priority<\/code> (100). Both advertise over VRRP, and the higher priority holds the VIP. <code>chk_haproxy<\/code> runs every two seconds. If HAProxy dies on the active host, its priority drops and the backup takes the VIP, so an HAProxy crash on one host no longer takes the entry point down with it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Point your applications at <code>10.0.0.1:5000<\/code> for writes and <code>10.5.5.100:5001<\/code> for reads. Your applications never see which physical HAProxy does the work.<\/p>\n\n\n\n<h1 class=\"wp-block-heading\">Summary<\/h1>\n\n\n\n<p class=\"wp-block-paragraph\">Patroni keeps the cluster healthy and picks the leader. HAProxy turns Patroni&#8217;s REST API into routing, sending writes to the primary and reads to the replicas by health-checking the API port while forwarding to the database port. The naive read-only listener drops reads when the last replica goes down, even though the primary could serve them. Adding <code>use_backend ... if { nbsrv(...) eq 0 }<\/code> with a primary-checking backend closes that gap, and a lag threshold on the replica check keeps stale standbys out of rotation. Keepalived puts a floating VIP in front of two HAProxy instances so the proxy layer survives a host failure too.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Writes reach the primary and reads spread across the replicas. Reads stay up as long as one node in the cluster is alive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let me know if you find any improvements to this configuration \ud83d\ude00 <\/p>\n","protected":false},"excerpt":{"rendered":"<p>Patroni runs your PostgreSQL cluster and handles failover, promoting a replica the moment the primary dies and recording the change in its distributed store (etcd, Consul, or ZooKeeper). That part works on its own. Your applications still need one stable address to connect to, and they need writes to reach the primary while reads spread [&hellip;]<\/p>\n","protected":false},"author":87,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[229,198,42],"tags":[1348,1491,4138,1024,1645,2602],"type_dbi":[2749],"class_list":["post-45342","post","type-post","status-publish","format-standard","hentry","category-database-administration-monitoring","category-database-management","category-operating-systems","tag-haproxy","tag-keepalived","tag-load","tag-load-balancer","tag-load-balancing","tag-postgresql-2","type-postgresql"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.8 (Yoast SEO v27.8) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Patroni HAProxy Configuration for HA &amp; Load Balancing<\/title>\n<meta name=\"description\" content=\"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived\" \/>\n<meta property=\"og:description\" content=\"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/\" \/>\n<meta property=\"og:site_name\" content=\"dbi Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-26T19:11:41+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-26T19:11:43+00:00\" \/>\n<meta name=\"author\" content=\"Joan Frey\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joan Frey\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"6 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/\"},\"author\":{\"name\":\"Joan Frey\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/#\\\/schema\\\/person\\\/c03c47649664fe73b27ce457e99f5b06\"},\"headline\":\"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived\",\"datePublished\":\"2026-06-26T19:11:41+00:00\",\"dateModified\":\"2026-06-26T19:11:43+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/\"},\"wordCount\":1341,\"commentCount\":0,\"keywords\":[\"HAProxy\",\"keepalived\",\"Load\",\"load balancer\",\"load balancing\",\"postgresql\"],\"articleSection\":[\"Database Administration &amp; Monitoring\",\"Database management\",\"Operating systems\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/\",\"url\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/\",\"name\":\"Patroni HAProxy Configuration for HA & Load Balancing\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/#website\"},\"datePublished\":\"2026-06-26T19:11:41+00:00\",\"dateModified\":\"2026-06-26T19:11:43+00:00\",\"author\":{\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/#\\\/schema\\\/person\\\/c03c47649664fe73b27ce457e99f5b06\"},\"description\":\"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/\",\"name\":\"dbi Blog\",\"description\":\"\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/#\\\/schema\\\/person\\\/c03c47649664fe73b27ce457e99f5b06\",\"name\":\"Joan Frey\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g\",\"caption\":\"Joan Frey\"},\"url\":\"https:\\\/\\\/www.dbi-services.com\\\/blog\\\/author\\\/joanfrey\\\/\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Patroni HAProxy Configuration for HA & Load Balancing","description":"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/","og_locale":"en_US","og_type":"article","og_title":"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived","og_description":"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.","og_url":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/","og_site_name":"dbi Blog","article_published_time":"2026-06-26T19:11:41+00:00","article_modified_time":"2026-06-26T19:11:43+00:00","author":"Joan Frey","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Joan Frey","Est. reading time":"6 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/#article","isPartOf":{"@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/"},"author":{"name":"Joan Frey","@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/c03c47649664fe73b27ce457e99f5b06"},"headline":"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived","datePublished":"2026-06-26T19:11:41+00:00","dateModified":"2026-06-26T19:11:43+00:00","mainEntityOfPage":{"@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/"},"wordCount":1341,"commentCount":0,"keywords":["HAProxy","keepalived","Load","load balancer","load balancing","postgresql"],"articleSection":["Database Administration &amp; Monitoring","Database management","Operating systems"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/","url":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/","name":"Patroni HAProxy Configuration for HA & Load Balancing","isPartOf":{"@id":"https:\/\/www.dbi-services.com\/blog\/#website"},"datePublished":"2026-06-26T19:11:41+00:00","dateModified":"2026-06-26T19:11:43+00:00","author":{"@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/c03c47649664fe73b27ce457e99f5b06"},"description":"A practical Patroni HAProxy configuration for PostgreSQL HA and load balancing, with a read-only fallback that keeps reads alive when replicas go down.","breadcrumb":{"@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.dbi-services.com\/blog\/highly-available-load-balanced-postgresql-with-patroni-haproxy-and-keepalived\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/www.dbi-services.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Highly Available, Load-Balanced PostgreSQL with Patroni, HAProxy, and Keepalived"}]},{"@type":"WebSite","@id":"https:\/\/www.dbi-services.com\/blog\/#website","url":"https:\/\/www.dbi-services.com\/blog\/","name":"dbi Blog","description":"","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.dbi-services.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.dbi-services.com\/blog\/#\/schema\/person\/c03c47649664fe73b27ce457e99f5b06","name":"Joan Frey","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/1e650cf665b4d44dd186355827c0b049d2f95c8cbb45fd10d4e7cb255be67ecb?s=96&d=mm&r=g","caption":"Joan Frey"},"url":"https:\/\/www.dbi-services.com\/blog\/author\/joanfrey\/"}]}},"_links":{"self":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts\/45342","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/comments?post=45342"}],"version-history":[{"count":13,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts\/45342\/revisions"}],"predecessor-version":[{"id":45399,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/posts\/45342\/revisions\/45399"}],"wp:attachment":[{"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/media?parent=45342"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/categories?post=45342"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/tags?post=45342"},{"taxonomy":"type","embeddable":true,"href":"https:\/\/www.dbi-services.com\/blog\/wp-json\/wp\/v2\/type_dbi?post=45342"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}