← All posts

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.

A client's TCP flow is hashed once on its first packet and pinned by 5-tuple to Firewall appliance A for the life of the connection; Gateway Load Balancer encapsulates each packet in GENEVE over UDP 6081 on the way in and strips it on the way out, with a second healthy appliance sitting idle for this particular flow

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.

Left: the fail-open default — Firewall appliance A goes unhealthy but the existing flow stays pinned to it, GWLB emits no RST or ICMP, and the client's TCP stack sits in retry and exponential backoff for 30 seconds to several minutes. Right: send_tcp_reset.on_unhealthy enabled — GWLB sends a TCP RST to the client as soon as the target is marked unhealthy, the client opens a new connection, and the new 5-tuple hash routes it to healthy Firewall appliance B

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.

Cover of Cloud Networking and Resilience
Apress Media, LLC

Order the book

Cloud Networking and Resilience

Designing Scalable, Fault-Tolerant, and Highly-Available Cloud Network Architectures

ISBN · Print 979-8-8688-2435-7
ISBN · eBook 979-8-8688-2436-4

Dedicated to Ade (2017–2022). All personal proceeds donated to charities supporting cats in distress.

Affiliate links — proceeds go to charities supporting cats in distress.