<p>If you’ve worked with VMware Horizon (now <a href="https://www.omnissa.com/products/horizon-8/" target="_blank">Omnissa Horizon</a>), you know it’s a common way for enterprise users to connect to remote desktops. But for IT engineers and DevOps teams? It’s a whole different story. Horizon’s custom protocols and complex connection requirements make <a href="http://production.int.haproxy.com/glossary/what-are-load-balancing-algorithms">load balancing</a> a bit tricky. </p><p>With its recent sale to Omnissa, the technology hasn’t changed—but neither has the headache of managing it effectively. Let’s break down the problem and explain why Horizon can be such a beast to work with… and how HAProxy can help.</p><h2 id="what-is-omnissa-horizon">What Is Omnissa Horizon?</h2><p>Horizon is a remote desktop solution that provides users with secure access to their desktops and applications from virtually anywhere. It is known for its performance, flexibility, and enterprise-level capabilities. Here’s how a typical Horizon session works:</p><ol start="1"><li><p><strong>Client Authentication:</strong> The client initiates a TCP connection to the server for authentication.</p></li><li><p><strong>Server Response:</strong> The server responds with details about which backend server the client should connect to.</p></li><li><p><strong>Session Establishment:</strong> The client establishes one TCP connection and two UDP connections to the designated backend server.</p></li></ol><p>The problem? In order to maintain session integrity, all three connections <em>must</em> be routed to the <strong>same backend server</strong>. But Horizon’s protocol doesn’t make this easy. The custom protocol relies on a mix of TCP and UDP, which have fundamentally different characteristics, creating unique challenges for load balancing.</p><h2 id="why-load-balancing-omnissa-horizon-is-so-difficult">Why Load Balancing Omnissa Horizon Is So Difficult</h2><h3 id="the-multi-connection-challenge">The Multi-Connection Challenge</h3><p>Since these connections belong to the same client session, they must route to the same backend server. A single misrouted connection can disrupt the entire session. For a load balancer, this is easier said than done.</p><h3 id="the-problem-with-udp">The Problem with UDP</h3><p><a href="http://production.int.haproxy.com/glossary/what-is-user-datagram-protocol-udp">UDP</a> is stateless, which means it doesn’t maintain any session information between the client and server. This is in stark contrast to <a href="http://production.int.haproxy.com/glossary/what-is-tcp">TCP</a>, which ensures state through its connection-oriented protocol. Horizon’s use of UDP complicates things further because:</p><ul><li><p>There’s no built-in mechanism to track sessions.</p></li><li><p>Load balancers can’t use traditional stateful methods to ensure all connections from a client go to the same server.</p></li><li><p>Maintaining session stickiness for UDP typically requires workarounds that add complexity (like an external data source).</p></li></ul><h3 id="traditional-load-balancing-falls-short">Traditional Load Balancing Falls Short</h3><p>Most load balancers rely on session stickiness (or affinity) to route traffic consistently. In TCP, this is often achieved with in-memory client-server mappings, such as with HAProxy's stick tables feature. However, since UDP is stateless and doesn't track sessions like TCP does, stick tables do not support UDP. Keeping everything coordinated without explicit session tracking feels like solving a puzzle without all the pieces—and that’s where the frustration starts. </p><p>This is why Omnissa (VMWare) suggests using their “<a href="https://techzone.omnissa.com/unified-access-gateway" target="_blank">Unified Access Gateway</a>” (UAG) appliance to handle the connections. While this makes one problem easier, it adds another layer of cost and complexity to your network. While you may need the UAG for a more comprehensive solution for Omnissa products, it would be great if there was a simpler, cleaner, and more efficient solution.<br /><br />This leaves engineers with a critical question: How do you achieve session stickiness for a stateless protocol? This is where HAProxy offers an elegant solution.</p><h2 id="enter-haproxy-a-stateless-approach-to-stickiness">Enter HAProxy: A Stateless Approach to Stickiness</h2><p>HAProxy’s<a href="http://production.int.haproxy.com/blog/client-ip-persistence-or-source-ip-hash-load-balancing"> <strong>balance-source algorithm</strong></a> is the key to solving the Horizon multi-protocol challenge. This approach uses consistent hashing to achieve session stickiness <em>without</em> relying on stateful mechanisms like stick tables. From the documentation:</p><blockquote><p><em>“The source IP address is hashed and divided by the total weight of the running servers to designate which server will receive the request. This ensures that the same client IP address will always reach the same server as long as no server goes down or up.” </em></p></blockquote><p>Here’s how it works:</p><ol start="1"><li><p><strong>Hashing Client IP:</strong> HAProxy computes a hash of the client’s source IP address.</p></li><li><p><strong>Mapping to Backend Servers:</strong> The hash is mapped to a specific backend server in the pool.</p></li><li><p><strong>Consistency Across Connections:</strong> The same client IP will always map to the same backend server.</p></li></ol><p>This deterministic, <em>stateless</em> approach ensures that all connections from a client—whether TCP or UDP—are routed to the same server, preserving session integrity.</p><h3 id="why-stateless-stickiness-works">Why Stateless Stickiness Works</h3><p>The beauty of HAProxy’s solution lies in its simplicity and efficiency—it has low overhead, works for both protocols and is tolerant to changes. Changes to the server pool may cause the connections to rebalance, but those clients will be redirected consistently as noted in the documentation:</p><blockquote><p><em>“If the hash result changes due to the number of running servers changing, many clients will be directed to a different server.”</em></p></blockquote><p>It is super efficient because there is no need for in-memory storage or synchronization between load balancers. The same algorithm works seamlessly for both TCP and UDP. </p><p>This stateless method doesn’t just solve the problem; it does so elegantly, reducing complexity and improving reliability.</p>
<img alt="HAProxy UDP" height="600" src="https://cdn.haproxy.com/img/containers/posts/2024/haproxy-enterprise-3-0/haproxy-3_0-udp-module.png/fa78f295a7a8373f805cea3d85f99463/haproxy-3_0-udp-module.png" width="1200" />
<h2 id="implementing-haproxy-for-omnissa-horizon">Implementing HAProxy for Omnissa Horizon</h2><p>While the configuration is relatively straightforward, we will need the <a href="http://production.int.haproxy.com/solutions/udp-load-balancing">HAProxy Enterprise UDP Module</a> to provide UDP load balancing. This module is included in HAProxy Enterprise, which adds additional enterprise functionality and ultra-low-latency security layers on top of our open-source core.</p>
<h3 id="implementation-overview">Implementation Overview</h3><p>So, how easy is it to implement? Just a few lines of configuration will get you what you need. You start by defining your frontend and backend, and then add the “magic”:</p><ol start="1"><li><p><strong>Define Your <a href="https://docs.haproxy.org/3.1/configuration.html#check">Frontend and Backend</a>:</strong> The <code>frontend</code> section handles incoming connections, while the <code>backend</code> defines how traffic is distributed to servers.</p></li><li><p><strong>Enable <a href="https://docs.haproxy.org/3.1/configuration.html#4-balance">Balance Source</a>:</strong> The <code>balance source</code> directive ensures that HAProxy computes a hash of the client’s IP and maps it to a backend server.</p></li><li><p><strong>Optimize <a href="https://docs.haproxy.org/3.1/configuration.html#check">Health Checks</a>:</strong> Include the <code>check</code> keyword for backend servers to enable health checks. This ensures that only healthy servers receive traffic.</p></li><li><p><strong>UDP Load Balancing</strong>: The <a href="https://customer-docs.haproxy.com/haproxy-enterprise/enterprise-modules/udp/?_gl=1*vgglcb*_gcl_au*OTM0OTczOTc0LjE3MzY5NjU0MTc.*_ga*MTI2MjIxMjYxLjE3MzY5NjU0MTk.*_ga_MGHPDQ7WFP*MTczNjk2NTQxOC4xLjEuMTczNjk2Njc4NC4wLjAuMA..">UDP module</a> in the enterprise edition is necessary for UDP load balancing, and uses the <code>udp-lb</code> keyword. </p></li></ol><p>Here’s what a basic configuration might look like for the custom <a href="https://techzone.omnissa.com/resource/network-ports-horizon-8#tunneled-connection" target="_blank">“Blast” protocol</a>:</p>
We are unable to display the code snippet, here is the URL: <a href="https://gist.github.com/rlnorthcutt/41e0549f02f7ac98adaf19c71290e204/#file-"></a>
<p>This setup ensures that all incoming connections—whether TCP or UDP—are mapped to the same backend server based on the client’s IP address. The <strong>hash-type consistent</strong> option minimizes disruption during server pool changes.</p><p>This approach is elegant in its simplicity. We use minimal configuration, but we still get a solid approach to session stickiness. It is also incredibly performant, keeping memory usage and CPU demands low. Best of all, it is highly reliable, with consistent hashing ensuring stable session persistence, even when servers are added or removed.</p><h2 id="refined-health-tracking-balancing-uag">Refined health tracking & balancing UAG</h2><p>While the basic configuration above works well, there are a few refinements and adjustments that can be added for a more comprehensive solution. In production-grade Omnissa Horizon environments, HAProxy is typically deployed in front of <strong>Unified Access Gateways (UAGs)</strong> rather than directly in front of internal Connection Servers. </p><p>This architecture places HAProxy at the edge to manage incoming external traffic before it enters the DMZ, ensuring that UAGs (which act as hardened proxies for internal VDI operations) remain secure and performant. There are a few key refinements we can add for this production-ready setup:</p><h3 id="synchronized-health-tracking">Synchronized health tracking</h3><p>While basic port checks verify network connectivity, they do not guarantee that the underlying Horizon application services are healthy. To solve this, use a dedicated health check backend like <code>be_uag_https</code> that specifically targets the <code>/favicon.ico</code> path. HAProxy can verify that all relevant UAG and Connection Server services are fully functional, not just that the port is open. </p><h3 id="long-lived-session-persistence">Long-lived session persistence</h3><p>Omnissa Horizon sessions are notably long-lived, with a default maximum duration of <strong>10 hours</strong>. Standard load balancer timeouts are often too aggressive, potentially severing active virtual desktop connections during a typical workday. To ensure stability, HAProxy can be configured with extended <code>timeout server</code> and <code>timeout client</code> settings of <strong>10 hours</strong> for all Blast and PCoIP backends. This aligns the load balancer’s persistence with the application’s session lifecycle, ensuring that even if a user is momentarily idle, their secondary protocols remain pinned to the correct UAG node.</p><h3 id="edge-security-and-ssl-bridging">Edge security and SSL bridging</h3><p>For external-facing deployments, HAProxy should serve as the first line of defense using advanced security features like <strong>WAF (Web Application Firewall)</strong> and <strong>Brute Force Detection</strong> on the initial authentication endpoints. This protects the environment from credential-stuffing and application-layer attacks before they ever reach the UAG. </p><p>Furthermore, because UAGs require end-to-end encryption for security, HAProxy should be configured for <strong>SSL Bridging</strong>. It is important to use the same SSL certificate on both the HAProxy virtual service and the UAG nodes.</p><p>This is crucial because the UAGs use fingerprinting for the certificate used for incoming requests, meaning the certificate presented by the HAProxy load balancer and the certificate on the UAG's outside interface must be the same to prevent certificate mismatch errors during the session handoff between the primary authentication and secondary display protocols.</p><h3 id="sample-configuration-with-uag-load-balancing-advanced-health-tracking">Sample configuration with UAG load balancing & advanced health tracking</h3><p>In this refined setup, the <code>be_uag_https</code> backend does the heavy lifting. All other backends simply "watch" its status. See the Omnissa documentation for a <a href="https://docs.omnissa.com/bundle/UnifiedAccessGatewayDeployandConfigureV2209/page/FirewallrulesforDMZ-basedUnifiedAccessGatewayappliances.html#tunnel_basic_endpoint_configuration" target="_blank">full list of port requirements</a> for the different services within Unified Access Gateway.</p>
We are unable to display the code snippet, here is the URL: <a href="https://gist.github.com/rlnorthcutt/de362c72f1bce4de778c58eb0b1adf56/#file-"></a>
<h3 id="understanding-the-track-directive-and-timing">Understanding the track Directive and Timing</h3><p>When you use the <code>track</code> keyword, the secondary servers <strong>inherit the state</strong> of the target. They don’t send their own health check packets, this enables further synchronicity<strong>:</strong> If <code>srv1</code> fails the favicon check, it is marked down for Blast TCP, Blast UDP, and PCoIP UDP at the exact same millisecond. </p><p>This prevents the "zombie session" issue. Without tracking, a user might be connected via TCP while their UDP media stream is hitting a dead server.</p><p>This centralized tracking approach transforms your health checks from a series of fragmented probes into a unified "source of truth" for your infrastructure. By anchoring every protocol to a single HTTP health check, you eliminate the risk of partial failures. A server that appears healthy for UDP while its TCP services are actually failing can't happen, and the client's entire session remains synchronized.</p><p>It's a configuration that's both more robust and significantly lighter on your backend resources, providing the stability required for high-performance virtual desktop environments.</p><h2 id="advanced-options-in-haproxy-30">Advanced Options in HAProxy 3.0+</h2><p><a href="http://production.int.haproxy.com/blog/announcing-haproxy-3-0">HAProxy 3.0</a> introduced enhancements that make this approach <em>even better.</em> It offers more granular control over hashing, allowing you to specify the <strong><a href="https://docs.haproxy.org/3.1/configuration.html#5.2-hash-key">hash key</a></strong> (e.g., source IP or source+port). This is particularly useful for scenarios where IP addresses may overlap or when the list of servers is in a different order.</p><p>We can also include <strong><a href="https://docs.haproxy.org/3.1/configuration.html#4.2-hash-balance-factor">hash-balance-factor</a></strong>, which will help keep any individual server from being overloaded. From the documentation:</p><blockquote><p><em>“Specifying a "hash-balance-factor" for a server with "hash-type consistent" enables an algorithm that prevents any one server from getting too many requests at once, even if some hash buckets receive many more requests than others. </em></p><p><em>[...]</em></p><p><em>If the first-choice server is disqualified, the algorithm will choose another server based on the request hash, until a server with additional capacity is found.”</em></p></blockquote><p>Finally, we can adjust the <strong><a href="https://docs.haproxy.org/3.1/configuration.html#4-hash-type">hash function</a></strong> to be used for the <code>hash-type consistent</code> option. This defaults to <code>sdbm</code>, but there are 4 functions and an optional <code>none</code> if you want to manually hash it yourself. See the documentation for details on these functions.</p><p>Sample configuration using advanced options:</p>
We are unable to display the code snippet, here is the URL: <a href="https://gist.github.com/rlnorthcutt/dbb82055e3aba4c1eccc30bdc45b3ca5/#file-"></a>
<p>These features improve flexibility and reduce the risk of uneven traffic distribution across backend servers.</p><h2 id="coordination-without-coordination">Coordination Without Coordination</h2><p>The genius of HAProxy’s solution lies in its <strong>stateless state</strong>. By relying on consistent algorithms, it achieves an elegant solution that many would assume requires complex session tracking or external databases. This approach is not only efficient but also scalable.</p><p>The result? A system that feels like it’s maintaining state without actually doing so. It’s like a magician revealing their trick—it’s simpler than it looks, but still impressive.</p><p>Understanding Omnissa Horizon’s challenges is half the battle. Implementing a solution can be surprisingly straightforward with HAProxy. You can ensure reliable load balancing for even the most complex protocols by leveraging stateless stickiness through consistent hashing.</p><p>This setup not only solves the Horizon problem but also demonstrates the power of HAProxy as a versatile tool for DevOps and IT engineers. Whether you’re managing legacy applications or cutting-edge deployments, HAProxy has the features to make your life easier.</p><hr /><h2 id="frequently-asked-questions-faqs">Frequently asked questions (FAQs)</h2>
<hr /><h3 id="resources">Resources</h3><ul><li><p>Blog post: "<a href="http://production.int.haproxy.com/blog/omnissa-horizon-alternative">Omnissa Horizon alternative: How HAProxy solves UDP load balancing</a>"</p></li><li><p>Blog post: “<a href="http://production.int.haproxy.com/blog/client-ip-persistence-or-source-ip-hash-load-balancing">Client IP Persistence or Source IP Hash Load Balancing</a>”</p></li><li><p>Blog post: “<a href="http://production.int.haproxy.com/blog/announcing-haproxy-3-0">Introducing HAProxy 3.0</a>”</p></li><li><p>Blog post: “<a href="http://production.int.haproxy.com/blog/load-balancing-radius-with-haproxy-enterprise-udp-module">Load Balancing RADIUS with HAPRoxy Enterprise UDP Module</a>”</p></li><li><p>Documentation: <a href="https://docs.haproxy.org/3.1/configuration.html#4-balance">Balance Source</a></p></li><li><p>Documentation: <a href="https://docs.haproxy.org/3.1/configuration.html#4.2-hash-type">Consistent Hashing</a></p></li><li><p>Documentation: <a href="https://docs.haproxy.org/3.1/configuration.html#check">Health Checks</a></p></li><li><p>Documentation: <a href="https://docs.haproxy.org/3.1/configuration.html#5.2-hash-key">Hash Key</a></p></li><li><p>Documentation: <a href="https://docs.haproxy.org/3.1/configuration.html#4.2-hash-balance-factor">Hash Balance Facto<strong>r</strong></a></p></li></ul>
The post <a href="https://www.haproxy.com/blog/load-balancing-vmware-horizons-udp-and-tcp">Load balancing VMware Horizon's UDP and TCP traffic: a guide with HAProxy</a> appeared first on <a href="https://www.haproxy.com">HAProxy Technologies</a>.