A keepAliveTimeout csapdája: miért 502-zik időnként a Node szerver a load balancer mögött

A Node.js HTTP szerverének keepAliveTimeout alapértéke nyolc éve öt másodperc – jóval rövidebb, mint a legtöbb load balancer vagy proxy idle timeoutja. Ez okozza a klasszikus, szórványos 502-es hibákat alacsony forgalom mellett. A Node.js most végre 65 másodpercre emeli az alapértéket (v26-tól), de addig kézzel kell beállítani.

Van egy klasszikus, nehezen reprodukálható hibaosztály, amivel szinte minden csapat találkozik, amikor egy Node.js API-t load balancer vagy reverse proxy mögé tesz: időnként, alacsony forgalom mellett, teljesen ok nélkül 502-t kap a kliens. A hiba nem jelentkezik terhelés alatt, nem jelentkezik lokálisan, és a Node process logjában semmi gyanús nincs. A jelenség oka egy nyolc éve változatlan Node.js alapérték: az http.Server.keepAliveTimeout, ami pontosan öt másodperc – jóval rövidebb, mint amit a legtöbb proxy vagy load balancer feltételez.

Ez idén tavasszal végre változik a Node.js-ben, de érdemes megérteni, mi történik pontosan, mert a javítás csak a jövő major verziótól kezdve segít – addig (és utána is, ha nem arra a verzióra frissítesz) neked kell kézzel beállítanod.

Diagram: kliens kapcsolódik egy load balancerhez, majd a load balancer és a Node.js szerver közötti kapcsolat egy eltérő időzítésű óra miatt megszakad

Mi az a keep-alive timeout, és miért fontos ki melyiket állítja

HTTP keep-alive mellett egy TCP-kapcsolatot több kérés-válasz pár is felhasznál egymás után, hogy elkerüljük az újabb TCP- és TLS-handshake költségét. A kapcsolatot valakinek le kell zárnia, ha már senki nem használja – ehhez mindkét oldal (kliens/proxy és szerver) tart egy saját inaktivitási timeoutot. A Node.js HTTP szerverén ezt a server.keepAliveTimeout vezérli: ennyi ideig vár tétlenül egy socketen az utolsó válasz elküldése után, mielőtt lezárja azt.

A dokumentáció szerint ez az érték 5000 ezredmásodperc – ellenőriztem is helyben:

$ node -e "console.log(require('node:http').createServer().keepAliveTimeout)"
5000

A gond az, hogy szinte minden előtte ülő komponens ennél jóval hosszabb idle timeoutot használ. Az AWS Application Load Balancer alapértelmezett idle timeoutja 60 másodperc, az nginx keepalive_timeout direktívájának alapértéke pedig 75 másodperc. A böngészők és a mobilkliensek szintén jóval öt másodpercnél hosszabb ideig tartanak nyitva egy connection poolban lévő socketet.

Így néz ki a verseny a gyakorlatban

Képzeld el, hogy a proxy és a Node között van egy kapcsolat, amin épp lezajlott egy kérés-válasz. Utána senki nem küld semmit rajta öt másodpercig – ez alacsony forgalmú endpointoknál teljesen hétköznapi. A Node ekkor lezárja a socketet, mert lejárt a keepAliveTimeout. A proxy erről nem tud: az ő idle timeoutja (60 vagy 75 másodperc) még bőven fut, szóval a connection poolban él tovább a kapcsolat mint „felhasználható”. Amikor a következő kliens kérés befut, a proxy fogja ezt a már halott socketet, és megpróbál ráírni. Az eredmény kliensoldalon 502 Bad Gateway, szerveroldalon egy ECONNRESET.

Ezt könnyen meg lehet figyelni magával a socket-lezárási viselkedéssel is. Az alábbi példa egy nyers TCP-kapcsolatot nyit, elküld egy kérést, majd szándékosan nem küld semmit tovább – csak figyeli, mikor zárja le a szerver a socketet:

import http from 'node:http';
import net from 'node:net';

const server = http.createServer({ keepAliveTimeout: 1000 }, (req, res) => {
  res.end('pong');
});

server.listen(0, () => {
  const { port } = server.address();
  const socket = net.connect(port, '127.0.0.1', () => {
    socket.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n');
  });

  const start = Date.now();
  socket.on('data', () => {
    console.log(`[t=${Date.now() - start}ms] válasz megjött, nem küldünk többet`);
  });
  socket.on('close', () => {
    console.log(`[t=${Date.now() - start}ms] a szerver zárta le a socketet`);
    server.close();
  });
});

Node 22-n futtatva:

[t=11ms] válasz megjött, nem küldünk többet
[t=2012ms] a szerver zárta le a socketet

Az 1000 ezredmásodperces keepAliveTimeout ellenére a socket ~2000 ms után záródik. Ennek oka egy másik, kevésbé ismert beállítás: a server.keepAliveTimeoutBuffer, ami a Node 22.19.0-ban (2025.08.28, lásd a V22-es changelogot) és a 24.6.0-ban jelent meg. Ez egy plusz, alapból 1000 ms-os puffert ad a tényleges socket-timeouthoz (socketTimeout = keepAliveTimeout + keepAliveTimeoutBuffer), kifejezetten azért, hogy csökkentse a felesleges ECONNRESET-eket – de a mögöttes probléma természetét ez nem változtatja meg, csak néhány másodperccel tolja el.

A bevett javítás – és ami most változik a Node.js-ben

A közösségben évek óta ismert workaround, hogy explicit módon nagyobbra állítjuk a keepAliveTimeout-ot a proxy idle timeoutjánál – AWS ALB mögött tipikusan 61000 ms-ra (lásd például Adam Crowder erről szóló írását):

const server = http.createServer(app);
server.keepAliveTimeout = 61_000; // > ALB idle timeout (60s)
server.headersTimeout = 65_000;   // legyen nagyobb, mint a keepAliveTimeout
server.listen(3000);

A headersTimeout (alapértéke 60000 ms) más célt szolgál – a HTTP fejlécek beolvasására vár, lassú kliensek elleni védelemként –, de mivel ugyanazon a socketen fut, célszerű a keepAliveTimeout fölé állítani, hogy ne fusson bele véletlenül egy újrafelhasznált kapcsolat közepén.

A Node.js maintainerei idén tavasszal végre a gyökerén kezelték a problémát. A #62782-es pull request 2026. április 26-án került be a main branchbe: a keepAliveTimeout alapértékét 5 másodpercről 65 másodpercre emeli. Az eredeti indoklás (a PR alapjául szolgáló #59193-as issue-ból) pontosan ezt a proxy/böngésző-inkompatibilitást nevezi meg okként. A változtatást a maintainerek lehetséges semver-major törésnek jelölték, ezért a tervek szerint a következő major kiadással, Node.js v26-tal érkezik majd, nem pedig backportolva a jelenlegi LTS-ágakba.

Ez azt jelenti, hogy ha most Node 20/22/24 LTS-en futtatsz proxy vagy load balancer mögötti szolgáltatást, ez a hiba ma is jelen van, és a Node.js-en belüli javítás csak akkor old meg bármit, ha ténylegesen v26-ra frissítesz – ami hosszabb távú terv, nem azonnali megoldás. A kézi beállítás tehát egyelőre nem opcionális, még ha a hosszú távú kilátás jó is.

Mire figyelj

  • Ha Node API-d load balancer, API gateway vagy reverse proxy mögött fut, nézd meg explicit van-e állítva a keepAliveTimeout – ha nincs, az 5 másodperces alapértéket kapod.
  • Állítsd az értéket a proxy réteg idle timeoutja fölé (nem alá!), és a headersTimeout-ot ez fölé.
  • Ez nem AWS- vagy nginx-specifikus hiba: bármelyik komponens, amelyik keep-alive kapcsolatot poolol a Node előtt (API gateway, service mesh sidecar, CDN origin-kapcsolat) ugyanezt a csapdát okozhatja, ha a saját idle timeoutja hosszabb, mint a Nodeé.
  • A tünet jellegzetesen szórványos, alacsony forgalom mellett gyakoribb (mert nagy terhelés alatt a kapcsolatok folyamatosan újrafelhasználódnak, mielőtt lejárna az öt másodperc) – éppen ezért nehéz staging környezetben reprodukálni.

A tanulság nem csak ez a konkrét beállítás, hanem az általános minta: ha egy elosztott rendszerben két komponens saját, egymástól független timeoutot tart ugyanarra a kapcsolatra, a kettő közti eltérés előbb-utóbb hibaként jelentkezik – és pont akkor, amikor a legnehezebb visszakövetni.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük