How to Set Up Load Balancing on FreeBSD
Load balancing is the practice of distributing incoming network traffic across multiple backend servers so that no single machine becomes a bottleneck. FreeBSD is exceptionally well suited for this role. Its kqueue event system, mature TCP/IP stack, and low-overhead networking primitives give load balancers running on FreeBSD a measurable performance advantage, which is why companies like Netflix and Juniper rely on FreeBSD at the network edge.
This guide covers three practical load balancing options on FreeBSD -- HAProxy, NGINX, and the native relayd daemon -- with complete, production-ready configuration examples. You will also learn about health checks, SSL termination, session persistence, and how to make the load balancer itself highly available using CARP.
Load Balancing Fundamentals
Before diving into configuration, it helps to understand the two layers at which load balancers operate and the algorithms they use to distribute traffic.
Layer 4 vs Layer 7
Layer 4 (transport layer) load balancers make routing decisions based on IP addresses and TCP/UDP port numbers. They do not inspect the content of the traffic. This makes them fast and protocol-agnostic -- they work for HTTP, database connections, mail servers, or any TCP/UDP service. The tradeoff is that they cannot make content-aware decisions like routing based on URL path or HTTP headers.
Layer 7 (application layer) load balancers understand the protocol being carried. For HTTP traffic, this means they can inspect headers, cookies, URL paths, and request methods. They can route /api/ requests to one backend pool and /static/ requests to another. They can also insert or modify headers, handle SSL termination, and perform content-based health checks.
HAProxy and NGINX both support Layer 4 and Layer 7 operation. relayd supports both as well, though its Layer 7 features are more limited.
Load Balancing Algorithms
The most commonly used algorithms are:
- Round Robin -- Requests are distributed to backends in sequential order. Simple and effective when backends have similar capacity.
- Least Connections -- New requests go to the backend with the fewest active connections. Better when request processing times vary.
- Source IP Hash -- The client IP is hashed to deterministically select a backend. Provides a basic form of session persistence without cookies.
- Weighted Round Robin -- Each backend is assigned a weight proportional to its capacity. A server with weight 3 receives three times the traffic of a server with weight 1.
- Random -- Selects a backend at random. Surprisingly effective at large scale due to the power of random choices.
- URI Hash -- The request URI is hashed to select a backend. Useful for cache-friendly routing where you want the same URL to always hit the same server.
HAProxy on FreeBSD
HAProxy is the most widely deployed open-source load balancer and reverse proxy. It was purpose-built for high-availability, high-throughput load balancing and handles millions of connections per second in production deployments worldwide. On FreeBSD, HAProxy uses kqueue for event-driven I/O, giving it excellent performance characteristics.
Installation
shpkg install haproxy sysrc haproxy_enable=YES
The main configuration file lives at /usr/local/etc/haproxy.conf.
Basic Configuration
HAProxy configuration is divided into four sections: global (process-wide settings), defaults (default values for all proxies), frontend (how incoming connections are received), and backend (where traffic is forwarded).
Here is a minimal configuration that load balances HTTP traffic across three backend web servers:
shellglobal log /var/run/log local0 maxconn 4096 user nobody group nobody daemon defaults log global mode http option httplog option dontlognull retries 3 timeout connect 5s timeout client 30s timeout server 30s frontend http_front bind *:80 default_backend web_servers backend web_servers balance roundrobin option httpchk GET /health http-check expect status 200 server web1 10.0.1.10:8080 check inter 5s fall 3 rise 2 server web2 10.0.1.11:8080 check inter 5s fall 3 rise 2 server web3 10.0.1.12:8080 check inter 5s fall 3 rise 2
Each server line defines a backend with health checking enabled. The check keyword activates health checks, inter 5s sets the interval to 5 seconds, fall 3 means a server is marked down after 3 consecutive failures, and rise 2 means it is marked up after 2 consecutive successes.
HAProxy Stats Page
HAProxy includes a built-in statistics dashboard that shows real-time connection counts, error rates, and backend health. Add this to your configuration:
shelllisten stats bind *:8404 stats enable stats uri /stats stats refresh 10s stats admin if LOCALHOST stats auth admin:your_secure_password
Access it at http://your-server:8404/stats. In production, restrict access to this page using your PF firewall rules or limit it to trusted networks.
Starting HAProxy
Validate the configuration and start the service:
shhaproxy -c -f /usr/local/etc/haproxy.conf service haproxy start
The -c flag performs a syntax check without starting the process. Always validate before restarting in production.
NGINX as a Load Balancer
NGINX is primarily known as a web server, but its reverse proxy and load balancing capabilities are production-grade. If you already run NGINX on FreeBSD as your web server, using it as a load balancer avoids adding another moving part to your infrastructure.
Installation
shpkg install nginx sysrc nginx_enable=YES
Upstream Blocks and Load Balancing Methods
NGINX defines backend server pools using upstream blocks. The load balancing method is set within the block:
nginxupstream web_backend { # Method: least_conn, ip_hash, or round-robin (default) least_conn; server 10.0.1.10:8080 weight=3; server 10.0.1.11:8080 weight=2; server 10.0.1.12:8080 weight=1; server 10.0.1.13:8080 backup; } server { listen 80; server_name example.com; location / { proxy_pass http://web_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_next_upstream error timeout http_502 http_503 http_504; proxy_connect_timeout 5s; proxy_read_timeout 30s; } }
The backup directive marks a server that only receives traffic when all primary servers are down. The weight directive controls traffic distribution -- server 10.0.1.10 receives three times as much traffic as 10.0.1.12. The proxy_next_upstream directive tells NGINX to retry the request on another backend if the first one returns an error or times out.
NGINX Health Checks
The open-source version of NGINX performs passive health checks by monitoring responses from backend servers. If a server returns errors or times out, NGINX temporarily stops sending it traffic:
nginxupstream web_backend { server 10.0.1.10:8080 max_fails=3 fail_timeout=30s; server 10.0.1.11:8080 max_fails=3 fail_timeout=30s; server 10.0.1.12:8080 max_fails=3 fail_timeout=30s; }
After 3 failures within 30 seconds, the server is marked as unavailable for the next 30 seconds. NGINX Plus (the commercial version) adds active health checks that proactively poll backends, similar to HAProxy.
relayd: The Native BSD Load Balancer
relayd is a load balancer and application layer gateway that ships with OpenBSD and is available on FreeBSD through ports. It was designed specifically for BSD systems and integrates tightly with PF for transparent redirection. If you prefer a BSD-native tool and your load balancing needs are straightforward, relayd is worth considering.
Installation
shpkg install relayd sysrc relayd_enable=YES
relayd.conf Configuration
The relayd configuration file uses a clean, readable syntax. Here is an example that load balances HTTP traffic across three backends with health checking:
shell# /usr/local/etc/relayd.conf # Macros web1 = "10.0.1.10" web2 = "10.0.1.11" web3 = "10.0.1.12" # Health check interval and timeout interval 10 timeout 1000 prefork 5 # Define the backend table with health checks table <webhosts> { $web1 $web2 $web3 } # HTTP health check table <webhosts> check http "/health" code 200 # Redirect incoming traffic to the backend pool redirect "web_traffic" { listen on 0.0.0.0 port 80 forward to <webhosts> port 8080 mode roundrobin check http "/health" code 200 } # Layer 7 relay with header manipulation relay "web_relay" { listen on 0.0.0.0 port 80 protocol http forward to <webhosts> port 8080 mode loadbalance check http "/health" code 200 }
The redirect block operates at Layer 4 (using PF rules under the hood), while the relay block operates at Layer 7 and can inspect and modify HTTP traffic. The mode keyword sets the balancing algorithm -- roundrobin, loadbalance (least states), hash, or random.
Start relayd:
shservice relayd start
relayd is simpler than HAProxy or NGINX, with fewer features, but its tight integration with PF and the BSD network stack makes it efficient for straightforward deployments.
SSL/TLS Termination at the Load Balancer
SSL termination means the load balancer handles TLS encryption/decryption so that backend servers receive plain HTTP. This simplifies certificate management (one certificate on the load balancer instead of one per backend) and offloads CPU-intensive cryptographic operations.
HAProxy SSL Termination
shellfrontend https_front bind *:443 ssl crt /usr/local/etc/haproxy/certs/example.com.pem http-request set-header X-Forwarded-Proto https # Redirect HTTP to HTTPS bind *:80 http-request redirect scheme https unless { ssl_fc } default_backend web_servers backend web_servers balance roundrobin server web1 10.0.1.10:8080 check server web2 10.0.1.11:8080 check server web3 10.0.1.12:8080 check
The .pem file must contain the certificate, any intermediate certificates, and the private key concatenated together. HAProxy reads them from a single file.
NGINX SSL Termination
nginxupstream web_backend { least_conn; server 10.0.1.10:8080; server 10.0.1.11:8080; server 10.0.1.12:8080; } server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name example.com; ssl_certificate /usr/local/etc/nginx/ssl/example.com.crt; ssl_certificate_key /usr/local/etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; location / { proxy_pass http://web_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; } }
For more details on NGINX SSL configuration, see our NGINX production setup guide.
Session Persistence
Some applications require that a user's requests consistently reach the same backend server, typically because session state is stored locally. There are several approaches to session persistence.
Source IP Affinity
The simplest method. The client's IP address determines which backend receives the request.
HAProxy:
shellbackend web_servers balance source hash-type consistent server web1 10.0.1.10:8080 check server web2 10.0.1.11:8080 check
NGINX:
nginxupstream web_backend { ip_hash; server 10.0.1.10:8080; server 10.0.1.11:8080; }
Source IP affinity breaks when clients are behind a shared NAT or proxy, because many users share the same source IP. It also rebalances poorly when backends are added or removed.
Cookie-Based Persistence
The load balancer inserts a cookie that identifies the backend server. This is more reliable than source IP because it works regardless of NAT.
HAProxy:
shellbackend web_servers balance roundrobin cookie SERVERID insert indirect nocache server web1 10.0.1.10:8080 check cookie s1 server web2 10.0.1.11:8080 check cookie s2 server web3 10.0.1.12:8080 check cookie s3
HAProxy will insert a SERVERID cookie with value s1, s2, or s3. Subsequent requests from the same client are routed to the server whose cookie value matches.
NGINX:
The open-source version of NGINX does not support cookie-based sticky sessions natively. NGINX Plus includes the sticky cookie directive. For open-source NGINX, use ip_hash or an external session store (Redis, Memcached) shared by all backends.
External Session Store
The most scalable approach is to avoid server-affinity entirely. Store sessions in a shared backend like Redis or a database. All application servers read and write session data from the same store. This lets the load balancer use any algorithm without worrying about session state, and simplifies scaling because you can add or remove backends freely.
Health Checks
Health checks are what separate a load balancer from a simple reverse proxy. Without health checks, failed backends continue receiving traffic and users see errors. There are three types.
TCP Health Checks
The load balancer opens a TCP connection to the backend. If the connection succeeds, the server is considered healthy. This is the simplest check and works for any TCP service.
HAProxy:
shellserver web1 10.0.1.10:8080 check inter 5s fall 3 rise 2
relayd:
shelltable <webhosts> check tcp
HTTP Health Checks
The load balancer sends an HTTP request and checks the response code. This verifies that the application is actually working, not just that the port is open.
HAProxy:
shellbackend web_servers option httpchk GET /health http-check expect status 200 server web1 10.0.1.10:8080 check
relayd:
shelltable <webhosts> check http "/health" code 200
Custom Health Checks in HAProxy
HAProxy supports advanced health check logic with multiple expectations:
shellbackend web_servers option httpchk http-check send meth GET uri /health ver HTTP/1.1 hdr Host example.com http-check expect status 200 http-check expect header Content-Type eq "application/json" server web1 10.0.1.10:8080 check inter 10s fall 3 rise 2
Your /health endpoint should check dependencies -- database connectivity, disk space, cache availability -- and return a non-200 status if any are failing.
High Availability for the Load Balancer Itself
A single load balancer is a single point of failure. FreeBSD provides two mechanisms to make the load balancer itself highly available: CARP and pfsync.
CARP (Common Address Redundancy Protocol)
CARP allows multiple FreeBSD hosts to share a virtual IP address. One host is the master and responds to traffic on the virtual IP. If the master fails, a backup host takes over within seconds. This is how you build an active/passive load balancer pair.
On the primary load balancer:
shifconfig carp0 create ifconfig carp0 vhid 1 advskew 0 pass secretpassword 10.0.1.1/24
On the secondary load balancer:
shifconfig carp0 create ifconfig carp0 vhid 1 advskew 100 pass secretpassword 10.0.1.1/24
Both machines share the virtual IP 10.0.1.1. The advskew value determines priority -- the lower value becomes master. When the primary goes down, the secondary detects the absence of CARP advertisements and takes over the virtual IP.
To persist this across reboots, add to /etc/rc.conf:
shcloned_interfaces="carp0" ifconfig_carp0="vhid 1 advskew 0 pass secretpassword 10.0.1.1/24"
pfsync for State Synchronization
If you use PF for connection tracking (which relayd relies on), pfsync synchronizes the state table between the primary and secondary machines. This means active connections survive a failover without being dropped.
shifconfig pfsync0 syncdev em1 syncpeer 10.0.2.2
Where em1 is a dedicated sync interface between the two load balancers. For a deeper dive into CARP, pfsync, and failover configurations, see our FreeBSD high availability guide.
Comparison: HAProxy vs NGINX vs relayd
| Feature | HAProxy | NGINX (open-source) | relayd |
|---|---|---|---|
| Layer 4 load balancing | Yes | Yes (stream module) | Yes |
| Layer 7 load balancing | Yes | Yes | Limited |
| Active health checks | Yes | Passive only (Plus has active) | Yes |
| SSL termination | Yes | Yes | Yes |
| Cookie-based persistence | Yes | Plus only | No |
| Stats/monitoring dashboard | Built-in | Stub status only | relayctl |
| Configuration complexity | Medium | Low-medium | Low |
| Throughput (high concurrency) | Excellent | Excellent | Good |
| WebSocket support | Yes | Yes | Limited |
| HTTP/2 backend support | Yes (2.4+) | Yes | No |
| Native BSD integration | No | No | Yes (PF, CARP) |
| Community/docs | Very large | Very large | Small (BSD-focused) |
| FreeBSD pkg available | Yes | Yes | Yes |
When to use which:
- HAProxy -- Best for dedicated load balancing. Superior health checking, real-time stats dashboard, cookie-based persistence, and the most granular traffic management. The right choice when load balancing is the primary job.
- NGINX -- Best when you need a load balancer and a web server in one process, or when your team already knows NGINX. If you follow our guide to choosing a web server, you may already have NGINX deployed.
- relayd -- Best for simple setups on BSD systems where you want minimal dependencies and tight PF integration. If you already manage your network with PF and CARP, relayd fits naturally.
Complete HAProxy Configuration Example
This is a production-ready HAProxy configuration for a web application with multiple backend pools, SSL termination, health checks, session persistence, rate limiting, and a stats page.
shell# /usr/local/etc/haproxy.conf global log /var/run/log local0 info maxconn 10000 user nobody group nobody daemon tune.ssl.default-dh-param 2048 ssl-default-bind-options ssl-min-ver TLSv1.2 ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384 defaults log global mode http option httplog option dontlognull option forwardfor option http-server-close retries 3 timeout connect 5s timeout client 30s timeout server 30s timeout http-request 10s timeout queue 30s default-server inter 5s fall 3 rise 2 # Stats dashboard listen stats bind *:8404 stats enable stats uri /stats stats refresh 10s stats admin if LOCALHOST stats auth admin:change_this_password stats show-legends # HTTP to HTTPS redirect frontend http_redirect bind *:80 http-request redirect scheme https code 301 # Main HTTPS frontend frontend https_front bind *:443 ssl crt /usr/local/etc/haproxy/certs/example.com.pem # Security headers http-response set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" http-response set-header X-Content-Type-Options nosniff http-response set-header X-Frame-Options DENY # Rate limiting: track requests per source IP stick-table type ip size 100k expire 30s store http_req_rate(10s) http-request track-sc0 src http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 } # Route based on path acl is_api path_beg /api/ acl is_static path_beg /static/ /images/ /css/ /js/ acl is_websocket hdr(Upgrade) -i websocket use_backend api_servers if is_api use_backend static_servers if is_static use_backend websocket_servers if is_websocket default_backend app_servers # Application servers with cookie-based persistence backend app_servers balance roundrobin cookie SERVERID insert indirect nocache httponly secure option httpchk GET /health http-check expect status 200 server app1 10.0.1.10:8080 check cookie app1 weight 3 server app2 10.0.1.11:8080 check cookie app2 weight 3 server app3 10.0.1.12:8080 check cookie app3 weight 2 server app4 10.0.1.13:8080 check cookie app4 backup # API servers with least-connections balancing backend api_servers balance leastconn option httpchk GET /api/health http-check expect status 200 http-request set-header X-Forwarded-Proto https server api1 10.0.2.10:9090 check server api2 10.0.2.11:9090 check server api3 10.0.2.12:9090 check # Static content servers backend static_servers balance roundrobin option httpchk GET /static/health.txt http-check expect status 200 server static1 10.0.3.10:8080 check server static2 10.0.3.11:8080 check # WebSocket servers backend websocket_servers balance source option httpchk GET /ws/health timeout server 3600s timeout tunnel 3600s server ws1 10.0.4.10:8080 check server ws2 10.0.4.11:8080 check
This configuration demonstrates content-based routing (API, static files, WebSockets each go to different backend pools), cookie-based session persistence for the application tier, rate limiting per source IP, and extended timeouts for WebSocket connections.
Complete NGINX Load Balancer Configuration Example
Here is the equivalent NGINX configuration for a multi-pool load balancer with SSL termination:
nginx# /usr/local/etc/nginx/nginx.conf worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 4096; use kqueue; multi_accept on; } http { # Logging log_format lb '$remote_addr - $upstream_addr [$time_local] ' '"$request" $status $body_bytes_sent ' '"$http_referer" "$http_user_agent" ' 'upstream_response_time=$upstream_response_time'; access_log /var/log/nginx/lb_access.log lb; error_log /var/log/nginx/lb_error.log warn; # Timeouts proxy_connect_timeout 5s; proxy_read_timeout 30s; proxy_send_timeout 30s; # Buffers proxy_buffer_size 128k; proxy_buffers 4 256k; proxy_busy_buffers_size 256k; # Backend pools upstream app_backend { least_conn; server 10.0.1.10:8080 weight=3 max_fails=3 fail_timeout=30s; server 10.0.1.11:8080 weight=3 max_fails=3 fail_timeout=30s; server 10.0.1.12:8080 weight=2 max_fails=3 fail_timeout=30s; server 10.0.1.13:8080 backup; } upstream api_backend { least_conn; server 10.0.2.10:9090 max_fails=3 fail_timeout=30s; server 10.0.2.11:9090 max_fails=3 fail_timeout=30s; server 10.0.2.12:9090 max_fails=3 fail_timeout=30s; } upstream static_backend { server 10.0.3.10:8080 max_fails=3 fail_timeout=30s; server 10.0.3.11:8080 max_fails=3 fail_timeout=30s; } upstream websocket_backend { ip_hash; server 10.0.4.10:8080; server 10.0.4.11:8080; } # Rate limiting limit_req_zone $binary_remote_addr zone=global:10m rate=10r/s; # HTTP to HTTPS redirect server { listen 80; server_name example.com; return 301 https://$host$request_uri; } # Main HTTPS server server { listen 443 ssl; server_name example.com; ssl_certificate /usr/local/etc/nginx/ssl/example.com.crt; ssl_certificate_key /usr/local/etc/nginx/ssl/example.com.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_prefer_server_ciphers on; # Security headers add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; # Rate limiting limit_req zone=global burst=20 nodelay; # API traffic location /api/ { proxy_pass http://api_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_next_upstream error timeout http_502 http_503 http_504; } # Static content location ~* ^/(static|images|css|js)/ { proxy_pass http://static_backend; proxy_set_header Host $host; proxy_cache_valid 200 1h; expires 1d; } # WebSocket connections location /ws/ { proxy_pass http://websocket_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; } # Default: application servers location / { proxy_pass http://app_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_next_upstream error timeout http_502 http_503 http_504; } } # Stub status for monitoring server { listen 127.0.0.1:8080; location /nginx_status { stub_status; allow 127.0.0.1; deny all; } } }
Test the configuration before reloading:
shnginx -t service nginx reload
Monitoring Load Balancers
A load balancer that you cannot observe is a liability. At minimum, you should monitor:
- Backend health -- How many backends are up vs down at any given time.
- Request rate -- Requests per second hitting the load balancer.
- Error rate -- Percentage of responses that are 4xx or 5xx.
- Latency -- Time from receiving the client request to delivering the backend response.
- Connection counts -- Current active connections and connection queue depth.
HAProxy Monitoring
HAProxy's built-in stats page provides all of these metrics visually. For programmatic access, enable the stats socket:
shellglobal stats socket /var/run/haproxy.sock mode 660 level admin
Then query it:
shecho "show stat" | socat stdio /var/run/haproxy.sock echo "show info" | socat stdio /var/run/haproxy.sock
HAProxy also exposes a Prometheus-compatible metrics endpoint when compiled with the Prometheus exporter module. On FreeBSD:
shellfrontend prometheus bind *:8405 http-request use-service prometheus-exporter if { path /metrics } no log
NGINX Monitoring
NGINX's stub_status module (shown in the configuration above) provides basic metrics. For richer data, use the nginx-module-vts third-party module or export metrics to Prometheus using the nginx-prometheus-exporter.
shpkg install nginx-prometheus-exporter
relayd Monitoring
Use relayctl to inspect relayd's state:
shrelayctl show summary relayctl show redirects relayctl show relays relayctl show hosts
These commands show which backends are up, current connection counts, and relay statistics. For alerting, script these commands with a cron job and trigger alerts when backends go down.
Frequently Asked Questions
Which load balancer should I use on FreeBSD?
Use HAProxy if load balancing is the primary function of the server and you need advanced features like cookie-based persistence, detailed statistics, or content-based routing across many backend pools. Use NGINX if you want a combined web server and load balancer, or your team already manages NGINX. Use relayd for simple, BSD-native setups that integrate with PF and CARP without external dependencies.
Can I load balance non-HTTP traffic on FreeBSD?
Yes. Both HAProxy and NGINX support Layer 4 (TCP/UDP) load balancing for any protocol. In HAProxy, set mode tcp in your frontend and backend. In NGINX, use the stream module. relayd also supports generic TCP relay. You can load balance database connections, mail servers, DNS, or any other TCP/UDP service.
How do I handle SSL certificates for multiple domains?
HAProxy supports SNI (Server Name Indication). Place certificate files in a directory and point HAProxy to it:
shellbind *:443 ssl crt /usr/local/etc/haproxy/certs/
HAProxy will automatically select the correct certificate based on the requested hostname. NGINX uses separate server blocks with different ssl_certificate directives for each domain.
What is the performance difference between HAProxy and NGINX for load balancing?
For most deployments, the performance difference is negligible. Both handle tens of thousands of concurrent connections efficiently on FreeBSD using kqueue. HAProxy is marginally more efficient for pure proxying workloads because it was purpose-built for that role. NGINX may use slightly more memory per connection because of its more general-purpose architecture. In practice, your backend application will be the bottleneck long before the load balancer is.
How do I test my load balancer configuration?
Start with haproxy -c -f /usr/local/etc/haproxy.conf or nginx -t to validate syntax. Then use curl to verify routing:
sh# Check which backend handled the request curl -v http://example.com/ # Verify sticky sessions curl -c cookies.txt -b cookies.txt http://example.com/ curl -c cookies.txt -b cookies.txt http://example.com/ # Load test pkg install vegeta echo "GET http://example.com/" | vegeta attack -duration=30s -rate=100 | vegeta report
Monitor the stats page (HAProxy) or access logs (NGINX) during testing to confirm traffic is distributed as expected.
How many backend servers can a FreeBSD load balancer handle?
There is no hard limit. HAProxy and NGINX both support hundreds of backend servers per pool. The practical limits are the number of file descriptors (increase kern.maxfiles and kern.maxfilesperproc in sysctl.conf), available memory for connection tracking, and network bandwidth. A single FreeBSD load balancer can realistically handle dozens of backend pools with hundreds of servers each.
Should I use active or passive health checks?
Active health checks (HAProxy, relayd) are preferred because they detect failures proactively -- before a user request hits a dead backend. Passive checks (NGINX open-source) only detect failures after a real user request fails. If you use NGINX and need active checks, either upgrade to NGINX Plus or add an external health checker like Consul or custom scripts that remove unhealthy backends from the upstream configuration.
Summary
FreeBSD provides a strong foundation for load balancing thanks to its kqueue event system, efficient network stack, and native support for CARP failover. The three main tools -- HAProxy, NGINX, and relayd -- cover the full spectrum from simple to complex deployments.
Start with a single load balancer and basic round-robin distribution. Add health checks immediately -- they are non-negotiable for production. Add SSL termination at the load balancer to simplify certificate management. When your traffic justifies it, deploy a second load balancer with CARP for high availability.
The configurations in this guide are production-ready starting points. Adjust backend counts, health check intervals, timeouts, and balancing algorithms based on your actual traffic patterns and application behavior.