The Zombie Flow: What AWS's Gateway Load Balancer TCP Reset Actually Fixes
When a firewall appliance behind AWS's Gateway Load Balancer dies, the connections running through it don't get an error. They get silence, sometimes for minutes, because GWLB's default failure mode is built to survive a health-check flicker, not a real outage. A new TCP Reset feature finally gives it a way to say so — but only once you accept a trade-off the old default let you ignore.
On 2 September 2026, AWS’s Networking & Content Delivery team published a fix for a failure mode it built into Gateway Load Balancer (GWLB) on purpose. The announcement’s own framing is almost apologetic: “until today,” it says, a failed firewall or inspection appliance behind a GWLB meant TCP connections could “hang while TCP retry mechanisms and exponential backoff ran their course, sometimes for minutes.” What actually happens when an appliance dies is stranger than an ordinary outage. GWLB doesn’t fail closed, and it doesn’t reroute the connection to a healthy target the way most load balancers would. By default, it keeps forwarding the exact same flow to the exact same broken target, indefinitely, until something else — a client’s own retry timer — gives up first. AWS’s own name for this is “fail open”, and the fix doesn’t remove it. It gives it a voice.
The problem: an outage that produces no error
A Gateway Load Balancer doesn’t work like the load balancers most engineers reach for first. It operates at layer 3, listening for every IP packet on every port and forwarding it to a virtual appliance — a firewall, an IDS, a NAT fleet — by wrapping it in a GENEVE header and sending it over UDP port 6081. The appliance inspects the packet, re-encapsulates it, and hands it back. None of that requires GWLB to understand TCP as a protocol with a beginning, middle, and end; it just needs to keep sending packets from the same connection to the same appliance, which it does by hashing the 5-tuple — source IP, source port, destination IP, destination port, protocol — once per flow and pinning the result for that flow’s entire life.
That pinning is also what breaks quietly. Per AWS’s own documentation: “by default, existing flows go to the same target unless the flow times out or is reset, regardless of the health and registration status of the target.” If every target in scope is unhealthy, GWLB doesn’t refuse the traffic either — it “picks a target at random and forwards traffic to it for the life of the flow, until it is either reset or has timed out,” and because that target is unhealthy, “traffic is dropped until that target becomes healthy again.” No TCP reset. No ICMP unreachable. GWLB is a GENEVE tunnel, not a TCP endpoint, so there’s no natural place for it to inject a “this connection is over” signal on the client’s behalf — it was never a party to the TCP handshake in the first place. The client and server just stop hearing from each other, and their own stacks fall back to retransmission and exponential backoff, which AWS’s post puts at “30 seconds to several minutes before abandoning the connection.” Meanwhile the target’s health check status, the CloudWatch metrics, the target group console — all of it correctly shows the appliance as unhealthy. The monitoring isn’t lying. It’s the traffic path that’s stuck talking to a machine that already failed.

The challenge: fail open is the safer default, until it isn’t
This isn’t an oversight. AWS’s documentation gives the actual reason
existing flows stay pinned to a target regardless of health: the
behaviour “facilitates connection draining, and accommodates 3rd party
firewalls that are sometimes unable to respond to health checks due to
high CPU usage.” A firewall doing deep packet inspection under a genuine
traffic spike can miss a couple of health probes without being remotely
close to down. If GWLB tore down every live connection the moment a
target missed a heartbeat, it would turn a benign load spike into a
self-inflicted outage — a worse failure mode than the one it’s supposedly
protecting against, and a particularly bad one for a device whose entire
job is to sit transparently in the path of every packet. So the default
for both target_failover.on_unhealthy and target_failover.on_deregistration
is no_rebalance: stop sending new flows to a target once it’s marked
down, but leave existing ones exactly where they are.
That bet is reasonable for a transient blip and wrong for a real failure — a crashed instance, a bad deployment, a kernel panic — and GWLB has no way to tell the two apart at the moment a flow was established. Nor, being a GENEVE tunnel rather than a TCP endpoint, did it have an obvious way to construct a reset on behalf of a connection it never terminated. Fixing that meant building the ability to originate a packet type GWLB otherwise never emits, and doing it carefully enough not to fire on every routine health-check flap the fail-open default exists to survive.
The solution: two flags, three triggers, and a hard trade-off
The fix ships as two new target group attributes,
send_tcp_reset.on_unhealthy.enabled and send_tcp_reset.on_deregistration.enabled,
both false by default:
aws elbv2 modify-target-group-attributes \
--target-group-arn <arn> \
--attributes \
Key=send_tcp_reset.on_unhealthy.enabled,Value=true \
Key=send_tcp_reset.on_deregistration.enabled,Value=true
— or, in the console, the “No rebalance and send TCP reset (recommended)” tile under a target group’s attributes. Once enabled, GWLB sends a TCP RST to the traffic sender in three scenarios, per AWS’s launch post: the target becomes unhealthy, the target is deregistered and its drain time elapses, or an existing flow receives a non-SYN packet after its TCP idle timeout has expired — which by default is 350 seconds, adjustable between 60 and 6,000 seconds, but only when the target group uses 5-tuple flow stickiness. That idle-timeout case matters on its own: before this launch, GWLB’s documented behaviour for a stale flow was to silently drop the next packet after the timeout, not reset it — this feature converts a second class of silent failure into a loud one, not just the appliance-death case the launch post leads with.
The requirements attached to the feature are as informative as the
feature itself. It only works with 5-tuple stickiness — the target
group’s own default — because a reset targets one specific flow, and the
coarser 3-tuple or 2-tuple stickiness keys pin by IP pair across many
flows at once, with no clean way to end one without disturbing the
others; AWS states outright that you can’t enable it when
stickiness.enabled is true. It’s also mutually exclusive with
target_failover set to rebalance, which solves the same underlying
problem by a different route — rehashing existing flows onto a healthy
target instead of ending them — so AWS forces a choice between rerouting
the connection and telling the client to reconnect, rather than letting
both mechanisms fire on the same flow. And it’s TCP-only; UDP flows,
which GWLB times out at a fixed, non-configurable 120 seconds, are
untouched.

The number that makes the trade-off concrete is detection time, and it’s
worth being precise about it because AWS’s own post and AWS’s own target
group documentation don’t agree on what “default” means here. A GWLB
target group’s raw defaults are a
10-second health check interval and an unhealthy threshold of 2
consecutive failures — a 20-second worst case to mark a target down.
AWS’s launch post instead walks through a more conservative worked
example: a 30-second interval and a threshold of 3, putting first-reset
delivery at roughly three minutes after failure; tune both down to a
5-second interval and a threshold of 2, and detection happens in 10
seconds with the reset delivered within 90. That gap between the raw
default and AWS’s own slower example isn’t carelessness — it’s the actual
price of this feature. Under the old fail-open default, a false-positive
health check failure cost nothing, because the connection kept running
regardless. With resets switched on, that same false positive now
actively kills every flow pinned to the target it briefly, wrongly,
flagged as down. Sizing the interval and threshold more conservatively
than you’d have bothered with before is the real trade-off, not a
footnote to skip past. A new CloudWatch metric,
TCP_ELB_Reset_Count, tracks how many reset packets the load balancer
generated, giving operators a way to see a failover happening instead of
inferring it from a spike in retry-related tickets — at no additional
charge, in every Region where GWLB runs.
The footprint
This is a resilience knob, not a security fix, and it rewards a narrower
audience than “everyone running GWLB.” A client that doesn’t already
retry cleanly on a connection error gets nothing from a faster reset — an
application that never handled ECONNRESET gracefully won’t handle it
better arriving in ninety seconds than never arriving at all. What this
buys, specifically, is for architectures where the caller already retries
or fails over on connection error and was previously burning that retry
budget waiting out a TCP timeout it never needed to hit.
It’s also worth remembering which GWLB this is. It’s the same load
balancer that AWS’s own
VPC Encryption Controls
singles out as structurally incompatible with enforce mode — one more
sign that the inspection VPC built around it is not a place to run on
whatever settings it happened to launch with. If you’re running a
firewall or NAT fleet behind a GWLB target group today, the concrete
action is an audit, not a blanket flip: check target_failover and
stickiness.type on every target group, decide deliberately whether the
downstream client actually benefits from a fast reset, and — if it does
— retune the health check interval and threshold to match the new cost
of getting a health check wrong, rather than inheriting values that were
only ever safe under the fail-open default you’re about to turn off.