Load balancing Exchange with HAProxy
During a migration to Exchange Online, our load balancer could not take the extra traffic without a costly upgrade. HAProxy on a small Debian VM did the job: the full configuration.
Infrastructure exchange exchange-online haproxy load-balancer linux microsoft networkContents
During a recent Exchange migration, I faced a problem familiar to many IT teams: traffic surged during mailbox synchronisation, and I needed to load balance Exchange reliably without paying for a commercial load balancer.
Why I needed a cheaper load balancer
During a bulk email migration to Exchange Online from Exchange Server 2016/2019, our load balancer could not handle the temporary surge in traffic without an expensive license upgrade. I did not want to pay for licenses for what was essentially a one-off event lasting a few weeks, but I still needed:
- High availability for users and migration tools
- The ability to distribute connections across several Exchange servers
- Minimal downtime and smooth cutover
That sounded like another job for HAProxy.
What is HAProxy?
HAProxy is an open-source, enterprise-grade load balancer and proxy. It is fast, flexible and, most importantly for this project, free. You can run it on a Linux VM, even with modest resources.
The solution: HAProxy in front of Exchange
Here’s what I did:
- Deployed HAProxy on a lightweight Linux Debian 12 VM.
- Configured it to listen on ports 443 (HTTPS) and 80 (HTTP), redirecting all HTTP traffic to HTTPS, because nobody should use HTTP.
- Terminated SSL at HAProxy (one certificate for the world).
- Forwarded requests to multiple Exchange 2016/2019 servers behind the scenes.
- Set up health checks and sticky sessions.
Migration tools, Outlook and webmail users all kept working, with no expensive hardware or licensing.
The configuration
This is a simplified version, with SSL termination, a backend per Exchange service with its own health check, and /ecp restricted to internal subnets:
# Global settings
global
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
maxconn 8000
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
ssl-default-bind-ciphers 'HIGH:!aNULL:!MD5'
# Default settings
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 90000
timeout server 90000
# Statistics backend
listen stats
bind *:8404
mode http
stats enable
stats uri /stats
stats refresh 10s
# HTTP to HTTPS redirect
frontend exchange_http
bind *:80
mode http
redirect scheme https code 301 if !{ ssl_fc }
# HTTPS frontend with path-based backend selection & /ecp IP restriction
frontend exchange_https
bind *:443 ssl crt /etc/ssl/private/haproxy.pem
mode http
option forwardfor
acl xmail hdr(host) -i webmail.mycompany.com autodiscover.mycompany.com
# /ecp subnet restriction
acl path_ecp path_reg -i ^/ecp(/|$)
acl allowed_ecp_src src 10.1.2.0/24 192.168.100.0/24
http-request deny if path_ecp !allowed_ecp_src
# Service path acls (case-insensitive)
acl path_autodiscover path_reg -i ^/autodiscover(/|$)
acl path_mapi path_reg -i ^/mapi(/|$)
acl path_rpc path_reg -i ^/rpc(/|$)
acl path_owa path_reg -i ^/owa(/|$)
acl path_activesync path_reg -i ^/microsoft-server-activesync(/|$)
acl path_ews path_reg -i ^/ews(/|$)
use_backend be_autodiscover if xmail path_autodiscover
use_backend be_mapi if xmail path_mapi
use_backend be_rpc if xmail path_rpc
use_backend be_owa if xmail path_owa
use_backend be_activesync if xmail path_activesync
use_backend be_ews if xmail path_ews
use_backend be_ecp if xmail path_ecp
default_backend be_default
# Dedicated backend for /autodiscover
backend be_autodiscover
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /autodiscover/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Dedicated backend for /mapi
backend be_mapi
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /mapi/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Dedicated backend for /rpc
backend be_rpc
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /rpc/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Dedicated backend for /owa
backend be_owa
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /owa/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Dedicated backend for /microsoft-server-activesync
backend be_activesync
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /microsoft-server-activesync/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Dedicated backend for /ews
backend be_ews
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /ews/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Backend for /ecp (restricted by IP in frontend)
backend be_ecp
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /ecp/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
# Default backend
backend be_default
mode http
balance source
stick-table type ip size 200k expire 30m
stick on src
option httpchk GET /owa/healthcheck.htm
http-check expect status 200
server exchange01 10.11.12.1:443 ssl verify none check
server exchange02 10.11.12.2:443 ssl verify none check
server exchange03 10.11.13.1:443 ssl verify none check
server exchange04 10.11.13.2:443 ssl verify none check
- The public certificate is only needed on the HAProxy VM. I use a SAN certificate for webmail and autodiscover.
- Health checks make sure only healthy Exchange servers get traffic.
- Sticky sessions by client IP keep each client on the same server. Exchange 2013 and later do not strictly require session affinity, but it keeps connections from bouncing between servers.
Lessons learned
- HAProxy handled the bulk migration and the user traffic without trouble, and no expensive upgrade was needed.
- For ongoing production use, I’d recommend tuning, monitoring, and using at least two HAProxy nodes for true redundancy.
- Remember to secure your Exchange and HAProxy servers, keep certificates up to date, and regularly test your failover.
- Soon, email will be Microsoft’s responsibility, and I will stop supporting it.
If you need a fast, free, and reliable way to load balance Exchange during migrations (or even permanently for small/medium orgs), HAProxy is an excellent option. It saved us a significant amount of money and performed reliably throughout a busy migration.