Skip to content
EdgeServers

Nginx vs HAProxy vs Envoy — an honest 2026 comparison

Three excellent proxies, three different sweet spots. Where we deploy each one for customers, and the failure modes that decide which to pick.

May 24, 2026 · 10 min · by Sudhanshu K.

Nginx vs HAProxy vs Envoy — an honest 2026 comparison

The least useful version of this article would be a feature checklist. All three of these proxies will do L7 routing, all three will terminate TLS, all three will balance load across a backend pool. On a checkbox grid, they look nearly identical. The interesting question is not "can it?" but "what does it cost you operationally when something goes wrong at 2am?"

We run all three in production for different customers. None of them is universally the right answer. This post is the honest, opinionated guide to where each one wins — informed by years of running them as a managed Nginx, HAProxy, and Envoy operator across AWS, GCP, Azure, and DigitalOcean.

The one-line summary

  • Nginx — best general-purpose L7 reverse proxy. Pick this unless you have a specific reason not to.
  • HAProxy — best pure load balancer when the routing logic is simple and the performance ceiling is the only thing that matters.
  • Envoy — best when you have a service mesh, dynamic configuration, or observability requirements that the other two can't meet without bolting on third-party tooling.

The rest of this post is the long version of why.

Nginx — the safe default

What Nginx is genuinely best at:

  • Static file serving (its original purpose, still unmatched)
  • Static-configuration reverse proxy with TLS termination
  • Caching layer in front of an origin (the proxy_cache family of directives is mature)
  • Mixed workloads where one binary handles static assets, reverse-proxy traffic, and a bit of routing logic

What pushes us away from Nginx for specific deployments:

Dynamic configuration is awkward. The Nginx open-source build re-reads its config on nginx -s reload. Reloads are graceful but they're not instantaneous, and a reload-on-every-deployment pipeline starts to feel clunky once you're past a few backend services. Nginx Plus has a dynamic config API. The open-source workarounds — lua-nginx-module, OpenResty, generated includes — work but add layers.

Observability is basic out of the box. Nginx exposes a small set of metrics via the stub_status module. Anything richer (per-upstream latency histograms, per-route error rates) requires the Plus build, the Prometheus exporter, or custom Lua. It works, but the others ship more.

HTTP/2 to upstream isn't there in open-source. Open-source Nginx can serve HTTP/2 to clients but only speaks HTTP/1.1 to backends. For most workloads, fine. For gRPC backends, it's a constraint you'll feel.

A typical Nginx deployment we run looks like this:

upstream app {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
    keepalive 64;
}
 
server {
    listen 443 ssl http2;
    listen 443 quic reuseport;
    add_header Alt-Svc 'h3=":443"; ma=86400' always;
 
    server_name app.example.com;
    # ... TLS config ...
 
    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
 
    location /static/ {
        root /var/www;
        expires 1y;
    }
}

It does TLS termination, HTTP/3, reverse-proxy, and static-asset serving — in roughly 20 lines of config. For 80% of the customer edges we run, this is more than enough, and the operational story (config file, nginx -t, reload) is one every SRE on earth already knows.

We also still see plenty of Apache HTTP Server deployments in the wild, mostly with PHP/mod_php legacy applications. Modern Nginx beats it on every axis we care about for new deployments, but migration is a project of its own — not always worth doing for a stable workload.

HAProxy — the load balancer's load balancer

HAProxy is the answer when the question is purely "balance L4 or L7 traffic across a backend pool, as fast and as reliably as possible."

What HAProxy is genuinely best at:

  • High-throughput L4 (TCP-mode) load balancing — the fast path is brutally efficient
  • Sticky sessions with sophisticated affinity rules (cookie, source IP, custom hash)
  • Health checks with rich semantics (active, passive, agent-based, scriptable)
  • Runtime API for config changes without restart (socat /var/lib/haproxy/admin.sock and you can drain backends, change weights, add servers, all live)

What pushes us away from HAProxy:

Not a web server. HAProxy doesn't serve static files, doesn't cache, doesn't do anything except balance. If you want one binary that handles "TLS termination + static files + a bit of proxying", you'll end up with Nginx in front of HAProxy or vice versa. Two layers, two configs, two failure modes.

Configuration syntax has a learning curve. HAProxy's config language is its own thing — section-based, with ACLs that read like a mini-language. Powerful but not intuitive to a team coming from Nginx.

No HTTP/3 in any production-ready form yet. HAProxy added HTTP/3 in 2.7 (2022) and it's been improving, but as of early 2026 it's still flagged "experimental" in their docs for high-traffic deployments. For HTTP/3 today, we use Nginx or Envoy.

A typical HAProxy use case we run: a customer with a large legacy app, where the routing rules are "send all traffic to the pool, balance round-robin, sticky by cookie, with deep health checks". 50,000 RPS sustained, single-digit-millisecond p99 added latency. HAProxy is the tool for that job.

frontend https_in
    bind *:443 ssl crt /etc/haproxy/certs/
    mode http
    option httplog
    default_backend app_pool
 
backend app_pool
    mode http
    balance roundrobin
    cookie SERVERID insert indirect nocache
    option httpchk GET /healthz
    http-check expect status 200
    server app1 10.0.1.10:8080 check cookie app1 maxconn 1000
    server app2 10.0.1.11:8080 check cookie app2 maxconn 1000
    server app3 10.0.1.12:8080 check cookie app3 maxconn 1000 backup

Concise, fast, and the runtime API means we can drain app1 for a deploy with a single socat command without touching the config file.

Envoy — the service mesh and the dynamic-config story

Envoy is the most ambitious of the three. It was built at Lyft for a microservices estate and adopted by Istio, Consul Connect, AWS App Mesh, and GCP's Anthos Service Mesh as the data plane. Its design assumptions are different from Nginx and HAProxy — Envoy expects to be configured by a control plane, not by hand.

What Envoy is genuinely best at:

  • Dynamic configuration via xDS APIs — Listeners, Routes, Clusters, Endpoints all hot-reload without process restart
  • HTTP/2 and HTTP/3 to upstreams (full bidirectional gRPC support is first-class)
  • Observability — per-route histograms, request/response logging, integration with OpenTelemetry, Prometheus, statsd all built in
  • Sidecar mode in a service mesh — running one Envoy per pod is its native deployment topology
  • Sophisticated traffic management — circuit breaking, outlier detection, traffic shifting (canary, blue-green) all defined declaratively

What pushes us away from Envoy:

Operational complexity. Envoy alone is fine. Envoy + a control plane (Istio, Consul) + a service mesh worldview is a substantial investment in learning, monitoring, and on-call competence. For a customer running 4 microservices on a single VPC, this is dramatic overkill. For a customer running 200 services across regions, it pays for itself.

Configuration verbosity. Envoy's config is YAML or JSON, generated by the control plane. The hand-written form is hundreds of lines for what Nginx does in twenty. You're not supposed to hand-write it — that's the point of the control plane — but it means you can't debug a misbehaving Envoy by vim-ing its config file in the way you can with Nginx.

Resource footprint. Envoy uses more memory and CPU per RPS than Nginx or HAProxy. Not catastrophically more, but enough that on a per-pod sidecar topology with 1000 pods, you notice it on the bill.

A typical Envoy deployment we run: a Kubernetes customer on Azure with AKS, running Istio for service-to-service mTLS, traffic shifting, and end-to-end tracing. The Envoy sidecars handle everything; the application code is unaware of the mesh. Adding a canary deployment is a control-plane change, not a config-file edit. The observability story is dramatic.

The decision matrix

Here is how we actually choose, in practice:

NeedPick
Static files + reverse proxy + TLS, single VM or two-tier appNginx
Maximum L4/L7 load balancing throughput, simple routing rulesHAProxy
Service mesh, dynamic config, microservices observabilityEnvoy
Caching reverse proxy in front of an originNginx
gRPC-to-gRPC routing with circuit breakingEnvoy
Edge TLS termination + HTTP/3 + the simplest possible operator storyNginx
Sticky sessions with sophisticated rules, 50k+ RPSHAProxy
You already run Kubernetes with IstioEnvoy (you're running it whether you picked it or not)

The most common failure mode we see is teams reaching for the most-sophisticated tool by default. A small team running a single backend service does not need Envoy. The operational overhead of a service mesh is real, and unless the requirements demand it, Nginx is faster to ship, easier to debug, and cheaper to run.

The second-most-common failure mode is the opposite — a team that has outgrown Nginx's static-config model trying to fake dynamic routing with Lua and reload-on-deploy pipelines. At that point, biting the bullet on Envoy (or Nginx Plus) saves real pain.

What we ship

For customers without a strong preference, our default is:

  • Edge: Nginx with HTTP/3 + dual-cert TLS, the config from the previous posts in this series
  • Internal load balancers: Nginx for HTTP, HAProxy for TCP/database/Redis fronting
  • Service mesh: Envoy via Istio if the customer is already on Kubernetes and the service count justifies it; otherwise we defer it

We don't religiously promote one. The choice falls out of the workload, the team's existing skills, and what the customer's cloud already gives them for free. (AWS ALB and GCP Cloud Load Balancing are also Envoy-based under the hood, for what it's worth — sometimes the right answer is "use the cloud's managed L7", and we'll tell you when.)

If you have an existing edge that you suspect is the wrong tool for the workload, that's a conversation we have routinely. The managed operations team can do a migration assessment without committing you to anything — it's about an hour of joint review and we'll tell you, honestly, whether switching is worth the project.

Sudhanshu K. is a Staff DevOps engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). He has migrated customer edges in both directions between all three of these proxies and has strong opinions about which migrations were worth it.