Proxy protocol is sent on health checks, where it carries nothing useful
Enable it on an NLB target group and the header is prepended to health check connections too. On those connections it contains no client information at all, so the feature adds nothing there and still marks every target unhealthy.
· Venerable Networks
Proxy protocol v2 solves a real problem. A Network Load Balancer can hide the client's address from the target, and prepending a header to the TCP stream is how you get it back without changing the network path.
The failure mode is that it is not a client-side setting. It is a property of the connection between the load balancer and the target, and health checks are also connections between the load balancer and the target.
From the target group attributes documentation:
After you enable proxy protocol, the proxy protocol header is also included in health check connections from the load balancer. However, with health check connections, the client connection information is not sent in the proxy protocol header.
And the consequence, on the same page:
Targets can fail health checks if they can't parse the proxy protocol header. For example, they might return the following error: HTTP 400: Bad request
Note what those two quotes say together. The header is present on health checks, and on health checks it contains no client information. So the feature provides zero value on those connections and can still break them. There is no upside being traded for the risk.
What it looks like
A plain HTTP server behind an NLB. Enable the attribute and every target goes unhealthy at once. Curl the target directly and it returns 200 with the right body. The security group is fine, the port is right, the health check path exists.
The evidence is in the application's own log:
10.120.1.204 - - [23/Sep/2026 15:52:41] code 400, message Bad request version ('\x00\r\nQUIT\n\x21\x11\x00\x0c')That \r\n\r\n\x00\r\nQUIT\n is the proxy protocol v2 signature — twelve bytes that exist specifically so a receiver can recognise the header. To a server that does not look for it, they are the first twelve bytes of a request line.
The source address is the load balancer's, and the entries arrive exactly one health check interval apart.
A 400 is a perfectly valid HTTP response and a failed health check. The target is healthy and reports unhealthy.
Why the reasoning that misses it is sound
"A client-facing setting cannot affect health checks, because health checks come from the load balancer rather than from clients" is a completely reasonable inference. It happens to be false for this attribute, and the general form is worth keeping:
Settings named for the client are often implemented on the target path, and health checks travel the target path.
Ask it of any load balancer setting: does this change what arrives at the target? If so, what does that do to the check?
Nothing validates it, and nothing can
The attribute is a claim about the target's capabilities, and AWS has no way to verify it. So the target group is created successfully, the plan looks trivial, and the consequences appear as health check failures ten seconds later across every target in the group.
Doing it correctly
Enabling proxy protocol is a coordinated change, not a toggle, and the ordering is not reversible without an outage in between.
- Add proxy protocol support to the application and deploy it. Most servers have it —
proxy_protocolin nginx,accept-proxyin HAProxy,proxy_protocolon an Envoy listener. - Confirm the deployed version handles both a bare request and a prefixed one.
- Only then enable the attribute.
Disabling it while the target requires it breaks things just as thoroughly, so treat it as a protocol migration in both directions.
If the application genuinely cannot be changed, run a separate health check port whose listener does not receive the header and point health_check.port at it rather than traffic-port.
The health check choice made this legible
Worth dwelling on, because it cuts against instinct. This failure was loud — no healthy targets, clients get nothing, found in minutes — specifically because the health check was HTTP with a status code matcher.
A TCP health check would have succeeded. The connection establishes, the load balancer is satisfied, the target is marked healthy, and every real request gets a 400. The failure would have moved from "no healthy targets" to "clients receive 400 from a healthy target", which is considerably worse and much harder to attribute.
An HTTP health check that inspects a status code is doing real work. Here it converted a silent data-plane failure into an obvious control-plane one, which is what you want a health check to do.
The cheaper alternative
If the reason for enabling proxy protocol was logging client addresses, check whether client IP preservation gets you there first. It puts the real address in the packet's source field, needs no application change, and is enabled by default for instance target groups.
It is not free either — it changes which addresses your target security groups have to permit, and with it disabled you get roughly 55,000 connections per unique target instead. Both options cost something; they just cost different things, and knowing which bill you are paying is the actual decision.
Two labs cover the pair. The health check that sent binary is this failure. The load balancer that hid its clients is client IP preservation breaking security group rules instead — and it also documents a place where two AWS pages state opposite defaults for that attribute, which is worth knowing before you rely on either.