Load Balancing Exchange with HAProxy
During a recent Exchange migration, I faced a challenge familiar to many IT pros: bandwidth usage surged during mailbox syncs, and I needed a way to load balance Exchange traffic reliably—without the high cost of commercial load balancers.
Why I needed a cheaper load balancer
During a bulk email migration to Exchange Online from Exchange Server 2016/2019, our load balancer couldn’t handle the temporary surge in traffic without a pricey license upgrade. I didn’t want to invest in expensive licenses for what was essentially a one-time 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’s when I thought, that’s another mission for HAProxy.
What is HAProxy?
HAProxy is an open-source, enterprise-grade load balancer and proxy. It’s 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).
- Redirect any http traffic to https, because none 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, sticky sessions, and HTTP-to-HTTPS redirects.
This allowed all migration clients, Outlook, and webmail users to keep working—no expensive hardware or licensing required.
Enough talk, show me the configuration
Here’s a simplified snippet showing SSL termination and backend server health checks:
# 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
- SSL certificate is only needed on the HAProxy VM (I use a SAN for webmail and autodiscover).
- Health checks ensure only live Exchange servers get traffic.
- Sticky sessions (by client IP) make sure connections don’t bounce between servers (required in Exchange world).
Lessons learned?
- HAProxy handled bulk migration and user traffic like a champ. No expensive upgrades required.
- 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, the email will be Microsoft responsibility, and I will move away from 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 a fantastic option. It saved us significant cost and delivered rock-solid performance for our busy migration window.