{"id":1725,"date":"2025-11-11T21:31:11","date_gmt":"2025-11-11T18:31:11","guid":{"rendered":"https:\/\/www.dchost.com\/blog\/zero%e2%80%91downtime-haproxy-layer-4-7-load-balancing-health-checks-that-matter-sticky-sessions-that-behave-and-clean-tls-passthrough\/"},"modified":"2025-11-11T21:31:11","modified_gmt":"2025-11-11T18:31:11","slug":"zero%e2%80%91downtime-haproxy-layer-4-7-load-balancing-health-checks-that-matter-sticky-sessions-that-behave-and-clean-tls-passthrough","status":"publish","type":"post","link":"https:\/\/www.dchost.com\/blog\/en\/zero%e2%80%91downtime-haproxy-layer-4-7-load-balancing-health-checks-that-matter-sticky-sessions-that-behave-and-clean-tls-passthrough\/","title":{"rendered":"Zero\u2011Downtime HAProxy: Layer 4\/7 Load Balancing, Health Checks that Matter, Sticky Sessions that Behave, and Clean TLS Passthrough"},"content":{"rendered":"<div class=\"dchost-blog-content-wrapper\"><p>So there I was, staring at a slow\u2011moving progress bar on a Friday release, knowing full well that one wrong reload could drop hundreds of users off a checkout page. You know that feeling, right? When traffic is hot, stakes are high, and the only thing between your app and a storm of angry messages is your load balancer behaving like a calm, well\u2011trained traffic cop. That night, HAProxy saved me. Not magically\u2014just because I finally learned how to treat it with the respect it deserves: good health checks, predictable stickiness, and a deployment flow that never cuts connections mid\u2011sentence.<\/p>\n<p>If you&#8217;ve ever wondered how to make HAProxy do zero\u2011downtime balance at both Layer 4 and Layer 7, how to keep sessions sticky without making future you cry, or how to pass TLS straight through when you don\u2019t want to terminate at the edge, you\u2019re in the right place. Think of this as the friendly field guide I wish I\u2019d had years ago: conversational, practical, and shaped by the kind of lessons you only learn when real users are on the line.<\/p>\n<div id=\"toc_container\" class=\"toc_transparent no_bullets\"><p class=\"toc_title\">\u0130&ccedil;indekiler<\/p><ul class=\"toc_list\"><li><a href=\"#A_Simple_Mental_Model_for_L4_vs_L7_And_Why_It_Matters\"><span class=\"toc_number toc_depth_1\">1<\/span> A Simple Mental Model for L4 vs L7 (And Why It Matters)<\/a><ul><li><a href=\"#Where_L4_fits_perfectly\"><span class=\"toc_number toc_depth_2\">1.1<\/span> Where L4 fits perfectly<\/a><\/li><li><a href=\"#Where_L7_earns_its_keep\"><span class=\"toc_number toc_depth_2\">1.2<\/span> Where L7 earns its keep<\/a><\/li><\/ul><\/li><li><a href=\"#Health_Checks_That_Actually_Tell_the_Truth\"><span class=\"toc_number toc_depth_1\">2<\/span> Health Checks That Actually Tell the Truth<\/a><ul><li><a href=\"#Bonus_Weights_slowstart_and_graceful_rejoin\"><span class=\"toc_number toc_depth_2\">2.1<\/span> Bonus: Weights, slowstart, and graceful rejoin<\/a><\/li><\/ul><\/li><li><a href=\"#Sticky_Sessions_Without_Regrets\"><span class=\"toc_number toc_depth_1\">3<\/span> Sticky Sessions Without Regrets<\/a><\/li><li><a href=\"#TLS_Termination_vs_TLS_Passthrough_And_Why_I_Use_Both\"><span class=\"toc_number toc_depth_1\">4<\/span> TLS Termination vs TLS Passthrough (And Why I Use Both)<\/a><ul><li><a href=\"#Terminating_TLS_at_HAProxy\"><span class=\"toc_number toc_depth_2\">4.1<\/span> Terminating TLS at HAProxy<\/a><\/li><li><a href=\"#Passing_TLS_through_without_decryption\"><span class=\"toc_number toc_depth_2\">4.2<\/span> Passing TLS through without decryption<\/a><\/li><\/ul><\/li><li><a href=\"#Zero_Downtime_Reloads_Drains_and_Rollouts_That_Dont_Drop_Users\"><span class=\"toc_number toc_depth_1\">5<\/span> Zero Downtime: Reloads, Drains, and Rollouts That Don\u2019t Drop Users<\/a><ul><li><a href=\"#Hitless_seamless_reloads\"><span class=\"toc_number toc_depth_2\">5.1<\/span> Hitless (seamless) reloads<\/a><\/li><li><a href=\"#Draining_connections_during_deploys\"><span class=\"toc_number toc_depth_2\">5.2<\/span> Draining connections during deploys<\/a><\/li><li><a href=\"#Bluegreen_in_real_life\"><span class=\"toc_number toc_depth_2\">5.3<\/span> Blue\/green in real life<\/a><\/li><\/ul><\/li><li><a href=\"#RealWorld_Recipes_Timeouts_WebSockets_gRPC_and_PROXY_Protocol\"><span class=\"toc_number toc_depth_1\">6<\/span> Real\u2011World Recipes: Timeouts, WebSockets, gRPC, and PROXY Protocol<\/a><ul><li><a href=\"#Timeouts_that_keep_long_connections_alive\"><span class=\"toc_number toc_depth_2\">6.1<\/span> Timeouts that keep long connections alive<\/a><\/li><li><a href=\"#WebSockets_headers_and_upgrades\"><span class=\"toc_number toc_depth_2\">6.2<\/span> WebSockets headers and upgrades<\/a><\/li><li><a href=\"#PROXY_protocol_carrying_the_real_client_IP\"><span class=\"toc_number toc_depth_2\">6.3<\/span> PROXY protocol: carrying the real client IP<\/a><\/li><\/ul><\/li><li><a href=\"#Observability_and_Runtime_Tweaks_Without_the_Panic\"><span class=\"toc_number toc_depth_1\">7<\/span> Observability and Runtime Tweaks Without the Panic<\/a><\/li><li><a href=\"#A_Few_Patterns_I_Lean_On_So_You_Dont_Need_to_Learn_Them_the_Hard_Way\"><span class=\"toc_number toc_depth_1\">8<\/span> A Few Patterns I Lean On (So You Don\u2019t Need to Learn Them the Hard Way)<\/a><\/li><li><a href=\"#Putting_It_All_Together_A_Clean_Practical_Layout\"><span class=\"toc_number toc_depth_1\">9<\/span> Putting It All Together: A Clean, Practical Layout<\/a><\/li><li><a href=\"#Little_Pitfalls_That_Sneak_In\"><span class=\"toc_number toc_depth_1\">10<\/span> Little Pitfalls That Sneak In<\/a><\/li><li><a href=\"#A_Quick_Nod_to_Backups_and_Runbooks\"><span class=\"toc_number toc_depth_1\">11<\/span> A Quick Nod to Backups and Runbooks<\/a><\/li><li><a href=\"#WrapUp_The_Calm_Path_to_It_Just_Works\"><span class=\"toc_number toc_depth_1\">12<\/span> Wrap\u2011Up: The Calm Path to \u201cIt Just Works\u201d<\/a><\/li><\/ul><\/div>\n<h2 id=\"section-1\"><span id=\"A_Simple_Mental_Model_for_L4_vs_L7_And_Why_It_Matters\">A Simple Mental Model for L4 vs L7 (And Why It Matters)<\/span><\/h2>\n<p>Let\u2019s start with a friendly picture. Picture a busy toll booth. Layer 4 is the guard who just checks the license plate and lets you through\u2014it\u2019s about connections, IPs, and ports. It\u2019s fast, it\u2019s lean, it doesn\u2019t care what\u2019s inside the car. Layer 7 is the guard who also asks where you\u2019re going, peeks at the map, and directs you to the right lane\u2014HTTP headers, paths, cookies, and more. It\u2019s smarter, which sometimes means more work, but it lets you route decisions on things the app actually understands.<\/p>\n<p>In my experience, I like to think in two passes. First: do we need to inspect content? If not\u2014say we\u2019re passing TLS straight to the app or handling TCP protocols like MySQL or Redis\u2014Layer 4 shines. Second: do we need to shape behavior using HTTP\u2014sticky sessions with cookies, path\u2011based routing, header checks, or smart health checks? That\u2019s Layer 7 territory.<\/p>\n<p>The trick is to combine them without stepping on your own toes. I\u2019ve seen teams try to do everything at L7 and then wonder why their TLS passthrough got weird, or push everything to L4 and lose out on the really useful HTTP tools. We\u2019ll mix and match in a way that keeps your setup understandable six months from now.<\/p>\n<h3><span id=\"Where_L4_fits_perfectly\">Where L4 fits perfectly<\/span><\/h3>\n<p>Any time you don\u2019t want to terminate TLS at HAProxy\u2014maybe compliance reasons, maybe you prefer end\u2011to\u2011end TLS into Nginx or your app server\u2014Layer 4 is your friend. You can still sniff SNI for routing decisions without decrypting. You can still keep deployments graceful. But you won\u2019t be doing header rewrites or cookie stickiness here, and that\u2019s okay.<\/p>\n<h3><span id=\"Where_L7_earns_its_keep\">Where L7 earns its keep<\/span><\/h3>\n<p>When you need control. Things like <strong>cookie\u2011based stickiness<\/strong>, <strong>intelligent health checks<\/strong> that call \/healthz, <strong>path routing<\/strong>, <strong>rate limiting<\/strong>, and even some upgrade niceties for WebSockets. This is where HAProxy becomes more than a switch\u2014it becomes the smart middle layer that keeps your app honest during deploys and spikes.<\/p>\n<h2 id=\"section-2\"><span id=\"Health_Checks_That_Actually_Tell_the_Truth\">Health Checks That Actually Tell the Truth<\/span><\/h2>\n<p>I\u2019ve been burned by \u201cgreen\u201d nodes that weren\u2019t actually healthy\u2014like a backend that accepted TCP but returned 500s on a key endpoint. That\u2019s the kind of thing that makes a good release go sour. The fix? Don\u2019t just check if a port is open. Check what your users actually rely on.<\/p>\n<p>For HTTP services, I almost always use an explicit health endpoint. App teams can keep it fast and deterministic, and we can test more than just \u201cis the app running?\u201d We can include basic dependencies\u2014database connectivity, cache reachability, background queue status\u2014without making it brittle. When it fails, I want to know quickly; when it recovers, I want it to rejoin gracefully.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Layer 7 HTTP health checks\nbackend app_http\n  mode http\n  balance roundrobin\n  option httpchk GET \/healthz\n  http-check expect status 200\n  default-server inter 2s fastinter 1s downinter 500ms rise 3 fall 2\n  server app1 10.0.0.11:8080 check\n  server app2 10.0.0.12:8080 check\n<\/code><\/pre>\n<p>Notice the rise and fall behavior. It doesn\u2019t flap on one bad response; it also doesn\u2019t wait forever to recover. I like to keep \u201cfastinter\u201d snappy when a server looks unstable\u2014get signal faster and reduce the chance that bad nodes take traffic again too quickly.<\/p>\n<p>For TLS passthrough or pure TCP protocols, I\u2019ll switch to L4 checks. You can still get surprisingly nuanced with L4 by watching connection behavior, but it\u2019s not a replacement for an actual HTTP probe.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Layer 4 TCP health checks (simple and quick)\nbackend app_tls_passthrough\n  mode tcp\n  balance source\n  option tcp-check\n  default-server check inter 2s rise 2 fall 2\n  server app1 10.0.0.21:443 check\n  server app2 10.0.0.22:443 check\n<\/code><\/pre>\n<p>When teams ask me why their users still felt hiccups during deploys, the answer is often that health checks didn\u2019t line up with reality. Your checks should mirror the path a real user takes\u2014if the checkout dependency is down, your health check should say so.<\/p>\n<h3><span id=\"Bonus_Weights_slowstart_and_graceful_rejoin\">Bonus: Weights, slowstart, and graceful rejoin<\/span><\/h3>\n<p>One trick I love is slowly increasing the weight of a server that just came back. It\u2019s like easing back into traffic instead of flooring it onto the highway.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">backend app_http\n  mode http\n  balance roundrobin\n  option httpchk GET \/healthz\n  http-check expect status 200\n  default-server inter 2s rise 3 fall 2 slowstart 30s\n  server app1 10.0.0.11:8080 check weight 50\n  server app2 10.0.0.12:8080 check weight 50\n<\/code><\/pre>\n<p>Slowstart helps smooth spikes right after a deploy when caches are cold and JITs are warming. Your users feel a whole lot less \u201cthumpy.\u201d<\/p>\n<h2 id=\"section-3\"><span id=\"Sticky_Sessions_Without_Regrets\">Sticky Sessions Without Regrets<\/span><\/h2>\n<p>Sticky sessions are one of those topics where what works on Tuesday blows up on Black Friday. I remember a retail client where we \u201csolved\u201d login consistency with IP\u2011based stickiness, only to discover later that mobile carrier NATs were pinning entire cities to a single backend. Oops. Lesson learned: choose your stickiness <strong>signal<\/strong> carefully and in context.<\/p>\n<p>At Layer 7, you have the most control. Cookie\u2011based stickiness is the classic approach. HAProxy can insert a cookie and route requests to the same server automatically. Simple, predictable, and friendly to most apps.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Cookie-based stickiness at L7\nbackend app_http\n  mode http\n  balance roundrobin\n  cookie SRV insert indirect nocache httponly secure\n  option httpchk GET \/healthz\n  http-check expect status 200\n  server app1 10.0.0.11:8080 check cookie s1\n  server app2 10.0.0.12:8080 check cookie s2\n<\/code><\/pre>\n<p>If you already manage your own session cookie, HAProxy can match it without issuing its own. You can also hash on headers, URLs, or user IDs, but keep an eye on hot keys. Consistent hashing helps when workloads can skew.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># URI hashing for cacheable endpoints\nbackend media_http\n  mode http\n  balance uri\n  hash-type consistent\n  server media1 10.0.1.11:80 check\n  server media2 10.0.1.12:80 check\n<\/code><\/pre>\n<p>At Layer 4, you don\u2019t get cookies. You\u2019re mostly left with source IP hashing. It\u2019s not bad, but you should be aware of where it can backfire\u2014NATed egress, office proxies, and load tests from a single source all change the game. If you must stick at L4 in a complex environment, consider routing by SNI (per domain) or upstream PROXY protocol so your upstream Nginx can do smarter stickiness post\u2011TLS termination.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># L4 stickiness by source (use with care)\nbackend app_tls_passthrough\n  mode tcp\n  balance source\n  server app1 10.0.0.21:443 check\n  server app2 10.0.0.22:443 check\n<\/code><\/pre>\n<p>One more truth: sticky sessions are a trade\u2011off. They can stabilize login flows and long\u2011lived shopping carts, but they can also create uneven load when one server gets a popular cohort. Use them where you need them, and consider ways to reduce the need\u2014centralized sessions, idempotent APIs, and moving state out of instance memory.<\/p>\n<h2 id=\"section-4\"><span id=\"TLS_Termination_vs_TLS_Passthrough_And_Why_I_Use_Both\">TLS Termination vs TLS Passthrough (And Why I Use Both)<\/span><\/h2>\n<p>This topic can spark long whiteboard sessions. Here\u2019s the way I usually guide teams: terminate TLS at HAProxy when you want to do smart HTTP stuff\u2014rate limits, cookie stickiness, header rewrites\u2014and you\u2019re comfortable managing certificates at the edge. Pass TLS through when you want end\u2011to\u2011end encryption to the app layer, or when your app demands specific TLS behaviors (ALPN quirks, client cert validation) handled upstream.<\/p>\n<h3><span id=\"Terminating_TLS_at_HAProxy\">Terminating TLS at HAProxy<\/span><\/h3>\n<p>Terminating at the edge lets you use the full power of Layer 7. You can also offload CPU cost from app nodes and keep cert management centralized. With modern ciphers and ALPN settings, you can serve both HTTP\/2 and HTTP\/1.1 comfortably.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">frontend https_in\n  mode http\n  bind :443 ssl crt \/etc\/haproxy\/certs\/ bundle crt-list \/etc\/haproxy\/crt-list.txt alpn h2,http\/1.1\n  http-response set-header Strict-Transport-Security &quot;max-age=63072000; includeSubDomains; preload&quot;\n  http-response del-header Server\n  acl host_api hdr(host) -i api.example.com\n  use_backend api_http if host_api\n  default_backend app_http\n<\/code><\/pre>\n<p>When you manage certificates, think about automation and governance. I\u2019m a big fan of using a proper pipeline for cert updates and reloads. If you\u2019re weighing certificate types for e\u2011commerce or SaaS, and when Wildcard or EV makes sense, I wrote a warm guide that can help: <a href=\"https:\/\/www.dchost.com\/blog\/en\/dv-ov-ev-ve-wildcard-ssl-arasinda-kaybolmadan-e%E2%80%91ticaret-ve-saaste-hangi-sertifika-ne-zaman\/\">choosing the right SSL certificate without the drama<\/a>.<\/p>\n<h3><span id=\"Passing_TLS_through_without_decryption\">Passing TLS through without decryption<\/span><\/h3>\n<p>With TLS passthrough, you keep the handshake intact to the backend. HAProxy can still peek at SNI (without decrypting) and send traffic to the right cluster. This is lovely when your upstream Nginx or app gateway does mTLS or custom auth at the edge.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">frontend tls_router\n  mode tcp\n  bind :443\n  tcp-request inspect-delay 5s\n  tcp-request content accept if { req_ssl_hello_type 1 }\n  acl sni_app req.ssl_sni -i app.example.com\n  acl sni_api req.ssl_sni -i api.example.com\n  use_backend be_app_tls if sni_app\n  use_backend be_api_tls if sni_api\n  default_backend be_default_tls\n\nbackend be_app_tls\n  mode tcp\n  balance source\n  server app1 10.0.0.21:443 check\n  server app2 10.0.0.22:443 check\n\nbackend be_api_tls\n  mode tcp\n  balance source\n  server api1 10.0.1.21:443 check\n  server api2 10.0.1.22:443 check\n<\/code><\/pre>\n<p>If you\u2019re exploring mutual TLS for internal services or admin paths, I\u2019ve got a step\u2011by\u2011step that keeps it calm: <a href=\"https:\/\/www.dchost.com\/blog\/en\/nginx-ve-caddyde-mtls-nasil-kurulur-mikroservislerde-sertifika-dogrulamanin-tatli-sirlari\/\">why mTLS and how to set it up cleanly<\/a>. You can terminate client certs at your upstream Nginx while HAProxy simply routes by SNI\u2014no decryption needed at the balancer.<\/p>\n<p>And if you\u2019re juggling WebSockets or gRPC behind a CDN, stick around; I\u2019ll share a practical note on timeouts in a minute. I also wrote a deep dive on keeping these protocols happy: <a href=\"https:\/\/www.dchost.com\/blog\/en\/cloudflare-ile-websocket-ve-grpc-yayini-nasil-hep-canli-kalir-nginx-timeout-keep%E2%80%91alive-ve-kesintisiz-dagitimin-sirlari\/\">WebSockets and gRPC behind Cloudflare, without the tears<\/a>.<\/p>\n<h2 id=\"section-5\"><span id=\"Zero_Downtime_Reloads_Drains_and_Rollouts_That_Dont_Drop_Users\">Zero Downtime: Reloads, Drains, and Rollouts That Don\u2019t Drop Users<\/span><\/h2>\n<p>Here\u2019s the thing about \u201czero downtime\u201d: it\u2019s not just one feature. It\u2019s a little orchestra of careful behaviors\u2014hitless reloads, connection draining, sticky logic that behaves during deploys, and health checks that lead traffic away at the right time.<\/p>\n<h3><span id=\"Hitless_seamless_reloads\">Hitless (seamless) reloads<\/span><\/h3>\n<p>Modern HAProxy supports master\u2011worker mode and socket inheritance so you can reload configs without dropping ongoing connections. It feels like magic the first time it works in production. The secret sauce is letting the new process take over the listeners while the old process finishes existing connections.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># \/etc\/haproxy\/haproxy.cfg\nglobal\n  master-worker\n  stats socket \/run\/haproxy\/admin.sock mode 660 level admin expose-fd listeners\n  tune.ssl.default-dh-param 2048\n  nbthread 4\n\ndefaults\n  log global\n  option dontlognull\n  timeout connect 5s\n  timeout client  60s\n  timeout server  60s\n  timeout tunnel  2h  # helps websockets\/grpc\n<\/code><\/pre>\n<p>Then use your init system\u2019s reload, not a hard restart. With systemd, a simple reload command hands off sockets cleanly. If you want to go deeper, the official docs on configuration and runtime API are worth a quiet Sunday read: <a href=\"https:\/\/www.haproxy.org\/download\/\" rel=\"nofollow noopener\" target=\"_blank\">HAProxy documentation and downloads<\/a> and <a href=\"https:\/\/www.haproxy.com\/blog\/seamless-reload-with-haproxy-no-more-hacks\/\" rel=\"nofollow noopener\" target=\"_blank\">seamless reloads explained<\/a>.<\/p>\n<h3><span id=\"Draining_connections_during_deploys\">Draining connections during deploys<\/span><\/h3>\n<p>Before you restart a backend node, mark it as draining so new traffic stops while existing sessions finish. This is the little habit that prevents 499s and abandoned carts. You can do it with the admin socket:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">echo &quot;set server app_http\/app1 state drain&quot; | socat stdio \/run\/haproxy\/admin.sock\n# After deploy and quick checks\necho &quot;set server app_http\/app1 state ready&quot; | socat stdio \/run\/haproxy\/admin.sock\n<\/code><\/pre>\n<p>Pair draining with <strong>on-marked-down shutdown-sessions<\/strong> to be decisive when a check fails, and <strong>slowstart<\/strong> when a server returns. It\u2019s like landing a plane with flaps out and a nice long runway.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">backend app_http\n  mode http\n  option httpchk GET \/healthz\n  http-check expect status 200\n  on-marked-down shutdown-sessions\n  default-server inter 2s rise 3 fall 2 slowstart 30s\n<\/code><\/pre>\n<h3><span id=\"Bluegreen_in_real_life\">Blue\/green in real life<\/span><\/h3>\n<p>I\u2019ve had great luck with \u201ctwo backends, one front door.\u201d Prepare your new version on a separate backend, warm it up with shadow traffic or synthetic requests, then flip which backend the frontend uses. If the logs get weird, flip back. No panic. If your infra is orchestrated with Terraform and friends, this fits nicely into a zero\u2011downtime workflow\u2014I shared how I wire DNS and deploys together here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/terraform-ile-vps-ve-dns-otomasyonu-cloudflare-proxmox-openstack-ve-sifir-kesinti-dagitim-nasil-bir-araya-gelir\/\">automating DNS and zero\u2011downtime deploys<\/a>.<\/p>\n<h2 id=\"section-6\"><span id=\"RealWorld_Recipes_Timeouts_WebSockets_gRPC_and_PROXY_Protocol\">Real\u2011World Recipes: Timeouts, WebSockets, gRPC, and PROXY Protocol<\/span><\/h2>\n<p>If there\u2019s one category that causes silent pain, it\u2019s timeouts. Healthy values make everything feel \u201cbuttery.\u201d Bad ones lead to mystery disconnects.<\/p>\n<h3><span id=\"Timeouts_that_keep_long_connections_alive\">Timeouts that keep long connections alive<\/span><\/h3>\n<p>For WebSockets and gRPC, raise the tunnel timeout generously. I often set it to an hour or more depending on your use case. Keep\u2011alive is important too if you\u2019re doing L7 termination. If you\u2019re behind a CDN like Cloudflare, align upstream timeouts with their expectations or you\u2019ll see odd drops.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">defaults\n  timeout client 60s\n  timeout server 60s\n  timeout tunnel 2h   # websockets\/grpc\n  option http-keep-alive\n<\/code><\/pre>\n<p>If your app uses HTTP\/2 (gRPC does), make sure ALPN includes h2 when you terminate TLS. For passthrough, your upstream must advertise ALPN\u2014HAProxy won\u2019t alter it.<\/p>\n<h3><span id=\"WebSockets_headers_and_upgrades\">WebSockets headers and upgrades<\/span><\/h3>\n<p>When HAProxy terminates TLS and speaks HTTP, make sure upgrade requests pass through untouched. Here\u2019s the gentle nudge I add so upgrade flows are happy and not re\u2011encoded strangely:<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">backend ws_app\n  mode http\n  option http-server-close\n  timeout server 2h\n  timeout connect 5s\n  server ws1 10.0.2.11:8080 check\n<\/code><\/pre>\n<p>If this world is your daily driver, I\u2019ve written a separate guide to keep <a href=\"https:\/\/www.dchost.com\/blog\/en\/cloudflare-ile-websocket-ve-grpc-yayini-nasil-hep-canli-kalir-nginx-timeout-keep%E2%80%91alive-ve-kesintisiz-dagitimin-sirlari\/\">WebSockets and gRPC happy behind Cloudflare<\/a>, with a focus on no\u2011drama timeouts and upgrades.<\/p>\n<h3><span id=\"PROXY_protocol_carrying_the_real_client_IP\">PROXY protocol: carrying the real client IP<\/span><\/h3>\n<p>When you pass TLS through, your upstream Nginx or app gateway might need the client\u2019s real IP. Enter PROXY protocol. HAProxy can add it, and your upstream can read it without losing TLS passthrough. Just ensure both ends agree or things get messy.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">frontend tls_router\n  mode tcp\n  bind :443\n  tcp-request inspect-delay 5s\n  tcp-request content accept if { req_ssl_hello_type 1 }\n  use_backend be_app_tls\n\nbackend be_app_tls\n  mode tcp\n  balance source\n  server app1 10.0.0.21:443 check send-proxy-v2\n  server app2 10.0.0.22:443 check send-proxy-v2\n<\/code><\/pre>\n<p>On Nginx, enable <strong>proxy_protocol<\/strong> on the listener and make sure your real IP extraction trusts only your HAProxy IPs. It\u2019s one of those features that feels invisible when done right\u2014and ruins your day when misaligned.<\/p>\n<h2 id=\"section-7\"><span id=\"Observability_and_Runtime_Tweaks_Without_the_Panic\">Observability and Runtime Tweaks Without the Panic<\/span><\/h2>\n<p>One of my guilty pleasures is peeking at HAProxy\u2019s runtime stats and making small adjustments mid\u2011incident. Not in a cowboy way\u2014more like a short screwdriver twist on a valve that was too tight.<\/p>\n<p>Expose the admin socket safely (local only, strict permissions) and get comfortable querying it during spikes. You can see which backends are hot, which servers are draining, and adjust weights temporarily to give a tired node a break.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\"># Watch server state and sessions\necho &quot;show servers state&quot; | socat stdio \/run\/haproxy\/admin.sock\n\n# Nudge a hot server down a bit\necho &quot;set server app_http\/app2 weight 30&quot; | socat stdio \/run\/haproxy\/admin.sock\n<\/code><\/pre>\n<p>Stick tables are another unsung hero. They\u2019re great for gentle per\u2011IP rate control on login endpoints or to prevent abuse. I don\u2019t love aggressive blocking as a default, but a small amount of shaping keeps everyone else fast and happy.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">frontend https_in\n  mode http\n  bind :443 ssl crt \/etc\/haproxy\/certs\/\n  stick-table type ip size 200k expire 10m store http_req_rate(10s)\n  http-request track-sc0 src\n  acl abuse sc_http_req_rate(0) gt 50\n  http-request deny if abuse\n  default_backend app_http\n<\/code><\/pre>\n<p>With runtime control, you can turn the dial up or down without a redeploy. If your team likes infrastructure as code (mine does), document your \u201cincident playbook\u201d for these tweaks so you\u2019re not inventing it under pressure. I\u2019ve shared how I write calm, repeatable runbooks here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/felaket-kurtarma-plani-nasil-yazilir-rto-rpoyu-kafada-netlestirip-yedek-testleri-ve-runbooklari-gercekten-calisir-hale-getirmek\/\">no\u2011drama DR plans and runbooks<\/a>.<\/p>\n<h2 id=\"section-8\"><span id=\"A_Few_Patterns_I_Lean_On_So_You_Dont_Need_to_Learn_Them_the_Hard_Way\">A Few Patterns I Lean On (So You Don\u2019t Need to Learn Them the Hard Way)<\/span><\/h2>\n<p>Over the years, certain patterns just keep paying rent. Think of these less like rigid rules and more like habits that make production feel boring\u2014in the best way.<\/p>\n<p>First, give your health checks a path to fail early in deploy scripts. I like to have a \u201cready\u201d flag that flips only after database migrations have run, caches have warmed, and the app\u2019s background workers are attached. When \/healthz says 200, mean it.<\/p>\n<p>Second, drain before deploy, and rejoin slowly. If you\u2019re running autoscaling, make sure nodes that scale down drain too. You\u2019d be surprised how many times instance termination scripts forget to tell HAProxy, and half of your slow session commits just vanish.<\/p>\n<p>Third, decide upfront where state lives. If your sticky sessions exist to mask stateful behavior, ask whether that state should live elsewhere\u2014Redis, a session store, or a token strategy. Sticky sessions are a tool, not a crutch. They work great; they just shouldn\u2019t be the only reason users stay happy.<\/p>\n<p>Fourth, practice reloads. I treat HAProxy reloads like fire drills\u2014roll keys, renew certs, shuffle weights, flip blue\/green\u2014so when the real day comes, it\u2019s not your first rodeo. If you use Let\u2019s Encrypt, script cert updates and seamless reloads; the official docs and blogs like the HAProxy seamless reload guide are genuinely helpful: <a href=\"https:\/\/www.haproxy.com\/blog\/seamless-reload-with-haproxy-no-more-hacks\/\" rel=\"nofollow noopener\" target=\"_blank\">a detailed walkthrough of seamless reloads<\/a>.<\/p>\n<p>Finally, zoom out sometimes. HAProxy is the traffic cop, but it\u2019s part of a little ecosystem: app servers, databases, and caches all carry their weight. If you enjoy database side tuning, I have a gentle guide on splitting reads and writes at the MySQL layer with a friendly tone: <a href=\"https:\/\/www.dchost.com\/blog\/en\/proxysql-ile-mysql-read-write-split-ve-baglanti-havuzu-woocommerce-laravel-icin-gercek-dunya-rehberi\/\">ProxySQL for read\/write split and pooling without the drama<\/a>. Different layer, same idea\u2014move traffic smartly where it needs to go.<\/p>\n<h2 id=\"section-9\"><span id=\"Putting_It_All_Together_A_Clean_Practical_Layout\">Putting It All Together: A Clean, Practical Layout<\/span><\/h2>\n<p>Let\u2019s combine the pieces into a simple, production\u2011flavored layout. You\u2019ll see edge TLS termination for the main app, a passthrough SNI router for a special subdomain that needs end\u2011to\u2011end TLS, cookie stickiness for session stability, and a playbook for graceful deploys.<\/p>\n<pre class=\"language-bash line-numbers\"><code class=\"language-bash\">global\n  master-worker\n  stats socket \/run\/haproxy\/admin.sock mode 660 level admin expose-fd listeners\n  nbthread 4\n\ndefaults\n  log global\n  option httplog\n  option dontlognull\n  timeout connect 5s\n  timeout client 60s\n  timeout server 60s\n  timeout tunnel 2h\n\n# L7 HTTPS termination for main app\nfrontend https_in\n  mode http\n  bind :443 ssl crt \/etc\/haproxy\/certs\/ alpn h2,http\/1.1\n  http-response set-header Strict-Transport-Security &quot;max-age=63072000; includeSubDomains; preload&quot;\n  http-response del-header Server\n  acl host_app hdr(host) -i www.example.com example.com\n  acl host_ws hdr(host) -i ws.example.com\n  use_backend ws_app if host_ws\n  use_backend app_http if host_app\n  default_backend app_http\n\nbackend app_http\n  mode http\n  balance roundrobin\n  cookie SRV insert indirect nocache httponly secure\n  option httpchk GET \/healthz\n  http-check expect status 200\n  on-marked-down shutdown-sessions\n  default-server inter 2s rise 3 fall 2 slowstart 30s\n  server app1 10.0.0.11:8080 check cookie s1\n  server app2 10.0.0.12:8080 check cookie s2\n\nbackend ws_app\n  mode http\n  option http-server-close\n  timeout server 2h\n  server ws1 10.0.2.11:8080 check\n\n# L4 SNI-based TLS passthrough for a special domain\nfrontend tls_passthrough\n  mode tcp\n  bind :8443\n  tcp-request inspect-delay 5s\n  tcp-request content accept if { req_ssl_hello_type 1 }\n  acl sni_secure req.ssl_sni -i secure.example.com\n  use_backend be_secure_tls if sni_secure\n  default_backend be_secure_tls\n\nbackend be_secure_tls\n  mode tcp\n  balance source\n  default-server check inter 2s rise 2 fall 2\n  server sec1 10.0.3.21:443 check send-proxy-v2\n  server sec2 10.0.3.22:443 check send-proxy-v2\n<\/code><\/pre>\n<p>Deploy flow? Mark app1 drain, deploy, mark ready, then repeat for app2. If you rotate certs, reload HAProxy hitlessly. If you feature\u2011toggle a new path, use L7 routing rules to canary traffic by header or hostname without touching upstreams. Simple, readable, and future\u2011you will thank you.<\/p>\n<h2 id=\"section-10\"><span id=\"Little_Pitfalls_That_Sneak_In\">Little Pitfalls That Sneak In<\/span><\/h2>\n<p>Three gotchas I see a lot. First: mixed trust chains. If you terminate TLS at the edge and re\u2011encrypt to the backend, make sure your upstream trusts the right CAs or uses pinned certs. A mismatch here leads to head\u2011scratching 502s. Second: health checks that hit the homepage. That route changes during marketing campaigns; your checks should live on a boring, purpose\u2011built path. Third: sticky sessions during autoscaling. When a node disappears, some \u201csticky\u201d users feel a blip. Keep the session store centralized or tolerate a fast re\u2011login flow for safety.<\/p>\n<p>And a bonus one: gRPC and HTTP\/2 behind older proxies. If you terminate TLS, be sure your alpn includes h2, and your upstream supports it. Otherwise, clients silently downgrade and everything feels slower.<\/p>\n<h2 id=\"section-11\"><span id=\"A_Quick_Nod_to_Backups_and_Runbooks\">A Quick Nod to Backups and Runbooks<\/span><\/h2>\n<p>You can do everything right and still have a bad day if you don\u2019t have a playbook for when the unexpected shows up. I keep a small checklist nearby: how to drain, how to reload, how to rotate a cert, how to back out a routing rule. It\u2019s the type of thing you don\u2019t want to invent live. If infrastructure hygiene is your jam, I shared my favorite \u201cfirst boot to ready\u201d blueprint here: <a href=\"https:\/\/www.dchost.com\/blog\/en\/bulutun-ilk-nefesi-cloud%E2%80%91init-ve-ansible-ile-tekrar-uretilebilir-vps-nasil-kurulur\/\">from blank VPS to reproducible services with cloud\u2011init + Ansible<\/a>.<\/p>\n<p>And for the tracing nerds (I say that with love, because I am one), HAProxy logs are only half the story. Correlate them with app traces for true insight. If you\u2019re curious about a friendly, real\u2011world approach to telemetry, this guide is for you: <a href=\"https:\/\/www.dchost.com\/blog\/en\/opentelemetry-ile-izlenebilirlik-laravel-ve-node-jste-jaeger-tempoya-uctan-uca-izler-nasil-kurulur\/\">tracing that feels like a conversation with your app<\/a>.<\/p>\n<h2 id=\"section-12\"><span id=\"WrapUp_The_Calm_Path_to_It_Just_Works\">Wrap\u2011Up: The Calm Path to \u201cIt Just Works\u201d<\/span><\/h2>\n<p>When people ask me why I still reach for HAProxy, I tell them it\u2019s because it lets me be boring in production. And boring is beautiful. With the right blend of Layer 4 and Layer 7, health checks that reflect reality, sticky sessions that are deliberate\u2014not accidental\u2014and a TLS strategy that suits each service, you can create a traffic layer that never steals the show.<\/p>\n<p>If you take anything from this, let it be this handful of habits. Decide up front what you need from L4 versus L7. Make your health checks honest. Treat sticky sessions as a tool, not a crutch. And practice graceful rollouts\u2014drain first, reload hitlessly, and let servers rejoin gently. Your users won\u2019t notice, and that\u2019s the point.<\/p>\n<p>And if you\u2019re the kind of person who enjoys connecting the dots, you might like pairing this with a cert strategy that won\u2019t bite you later and some calm infra automation to glue it all together. Between a little planning and HAProxy\u2019s quietly powerful features, zero downtime stops being a slogan and starts being your default.<\/p>\n<p>Hope this was helpful! If you\u2019ve got questions or want me to dig into your specific setup, send them my way\u2014I love tuning these flows until they feel silky. See you in the next post.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"<p>So there I was, staring at a slow\u2011moving progress bar on a Friday release, knowing full well that one wrong reload could drop hundreds of users off a checkout page. You know that feeling, right? When traffic is hot, stakes are high, and the only thing between your app and a storm of angry messages [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1726,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26],"tags":[],"class_list":["post-1725","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-teknoloji"],"_links":{"self":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1725","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/comments?post=1725"}],"version-history":[{"count":0,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/posts\/1725\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media\/1726"}],"wp:attachment":[{"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/media?parent=1725"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/categories?post=1725"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.dchost.com\/blog\/en\/wp-json\/wp\/v2\/tags?post=1725"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}