Ez a modul katalógus, nem elbeszélés — arra való, hogy visszalapozz hozzá, amikor valami érthetetlen történik. A meglepetések nem véletlenszerűek: hat gyökérokra vezethetők vissza, és ha a hatot érted, a hetvenedik buktatót is ki fogod találni magadtól.
A workerd nem Node, hanem egy V8-alapú, böngészőhöz közelebb álló sandbox. A nodejs_compat flag sokat pótol belőle, de nem mindent, és a „nem mindent” pontos alakja évről évre változik. A 2026-os állapot:
| Állapot | Modulok |
|---|---|
| Teljes | assert, async_hooks, buffer, crypto, errors, events, fs, globals, http, https, net, path, process, punycode, querystring, stream, string_decoder, timers, url, util, zlib |
| Részleges | console, dns, module, os, perf_hooks, test, tls |
| Stub (importálható, de nem működik) | http2, vm, cluster, domain, trace_events, wasi, dgram, inspector, sqlite, child_process, readline, repl, tty, v8, worker_threads |
Az egész csak nodejs_compat flaggel és 2024-09-23 vagy későbbi compatibility_date-tel él.
try { const w = require('worker_threads'); useParallel() } catch { useSerial() }. Régen az import elhasalt, a catch ág lefutott, és a könyvtár a soros útra váltott — működött. Most az import sikerül (a stub létezik), a felderítés azt hiszi, van párhuzamosítás, és a hiba csak a tényleges hívásnál jelenik meg — mélyebben, homályosabb hibaüzenettel. Ha egy könyvtár korábban ment és egy compatibility_date-emelés után elromlott, itt keresd.eval tiltása — és amit magával rántBiztonsági okból nem engedélyezett az eval(), a new Function, és a bufferből történő WebAssembly-fordítás. Ez nem konfigurálható.
EvalError: Code generation from strings disallowed for this context
Ez a hibaüzenet a leggyakoribb „miért nem megy ez a könyvtár?” válasza. Bárminek eltörik, ami futásidőben állít elő JavaScriptet: a JSON Schema-validátorok jelentős része (az AJV sémából kódot fordít), a futásidőben fordító sablonmotorok, néhány minifier és kifejezés-kiértékelő.
node:fs valóságaA táblázatban a fs zölden szerepel, és ez félrevezető lehet: nem a lemezed van mögötte, hanem egy memóriában élő virtuális fájlrendszer.
| Útvonal | Mi ez |
|---|---|
/bundle | a Workered bundle-jébe csomagolt modulok, csak olvasható |
/tmp | írható ideiglenes könyvtár — kérésenként külön és üres |
/dev | /dev/null, /dev/random, /dev/zero, /dev/full |
Két dolgot érdemes tudni róla. Egyrészt a /tmp tartalma nem marad meg kérések között, és nem látható más, párhuzamosan futó kérésekből — vagyis ideiglenes fájlra építő könyvtár működni fog, de „töltsük fel egyszer, aztán használjuk” cache-elés nem. Másrészt a fájlok a memórialimitedbe számítanak, fájlonként legfeljebb 128 MB, és minden művelet szinkron. A fájlidőbélyegek mindig 1970-01-01 — ami néhány könyvtárat meg tud lepni.
Ez a 6. modul központi témája volt, itt csak a katalógus-bejegyzés. A Worker nem indul újra kérésenként: ugyanaz az isolate sok kérést szolgál ki, aztán bármikor eldobható.
| Következmény | Tünet |
|---|---|
| Modul-scope állapot megosztott | A felhasználó más adatait látja; „néha rossz a tenant” |
| Modul-scope inicializálás minden hidegindításkor fut | Szórványos lassú kérések; indulási limit túllépése |
| Nincs garancia a folytonosságra | A memóriában tartott cache „véletlenszerűen” üres |
Nincs process.on('exit')-szerű életciklus | A „takarítsunk le leálláskor” minta sosem fut le |
init(apiKey)-t vár egyszer, és utána globálisan használható, gyanakodj: kérésenként hozd létre, vagy ellenőrizd, hogy a példány valóban állapotmentes-e.Ez a legmeglepőbb tétel a listán, és a legtöbb fejlesztő akkor találkozik vele, amikor egy mérés következetesen nullát ad. A Cloudflare Spectre-védelmi modellje kimondja:
Date.now() a legutóbbi I/O idejét adja vissza. Kódfutás közben nem halad előre.” Vagyis két Date.now() hívás között, ha nem történt I/O, ugyanazt az értéket kapod — akármennyi számítást végeztél közben. Ez nem hiba, hanem szándékos: időzítéses oldalcsatorna-támadások ellen véd.// ez MINDIG 0-t ír ki Workers-en
const t0 = Date.now()
for (let i = 0; i < 1e7; i++) { szamol(i) }
console.log(Date.now() - t0) // → 0
// ez viszont VÉGTELEN CIKLUS — a feltétel sosem válik hamissá
const hatarido = Date.now() + 100
while (Date.now() < hatarido) { /* pörgő várakozás */ }
// ez viszont MŰKÖDIK: az await I/O, tehát az óra lép
await new Promise(r => setTimeout(r, 100))
| Amit eltör | Miért | Helyette |
|---|---|---|
| Kódon belüli időmérés | a különbség mindig 0 | a 16. modul CPU-profilja a DevToolsban |
Pörgő várakozás (while (Date.now() < x)) | végtelen ciklus → CPU-limit | await new Promise(r => setTimeout(r, ms)) |
| Szinkron backoff-hurok újrapróbálkozásban | ugyanaz a végtelen ciklus | await-es backoff |
| Szinkron blokkon belüli rate limiter | minden esemény ugyanazt az időbélyeget kapja | Durable Object vagy a Rate Limiting binding |
| Egyediségre használt időbélyeg | egy kérésen belül ütközik | crypto.randomUUID() |
new Date()-et neveztük meg — a szerver és a kliens más időt lát. Most kapott egy második indokot is: a szerveren az idő nem is halad a renderelés alatt, tehát még a szerveren belüli két hívás sem ad feltétlenül különböző értéket. Az időt vagy fentről add be propként, vagy kliensoldalon számold — de ne várd, hogy az SSR-renderelés „mérje” magát.A limitek nem lassulást jelentenek, hanem kemény falat: a kérés elhasal. Egy helyen, hivatkozásként:
| Erőforrás | Free | Paid | Túllépéskor |
|---|---|---|---|
| Worker-méret (gzip) | 3 MB | 10 MB | a deploy elutasítva |
| Worker-méret (tömörítetlen) | 64 MB | a deploy elutasítva | |
| Indulási idő | 1 másodperc | a Worker el sem indul | |
| CPU-idő / kérés | 10 ms | 30 s (5 percig emelhető) | a kérés megszakítva |
| Memória / isolate | 128 MB | OOM, az isolate eldobva | |
| Alhívás (subrequest) | 50 | 1 000 | a további fetch-ek elhasalnak |
| Egyidejű kimenő kapcsolat | 6 | a többi vár, amíg felszabadul | |
| Statikus fájlok száma | 20 000 | 100 000 | a deploy elutasítva |
| Statikus fájl mérete | 25 MiB | a deploy elutasítva | |
Promise.all-lal, azok nem futnak igazán párhuzamosan: hatosával haladnak. Az 5. modul „párhuzamosíts” tanácsa tehát hatig igaz, utána sorbaállás van. Ha tíz forrásból kell adat egy oldalhoz, az nem hangolási kérdés, hanem architekturális jelzés: kell egy aggregáló végpont vagy egy cache-réteg.| Tároló | Amit feltételeznél | Ami valójában van |
|---|---|---|
| KV | írás után azonnal olvasható | eventual consistency — az írás globális terjedése időbe telik |
| Cache API | globális cache | adatközpontonként külön; a cache.delete csak a helyi PoP-ot üríti |
| D1 | bárhonnan egyforma | van elsődleges régió; a távoli írások lassabbak |
| R2 | fájlrendszer | objektumtár — nincs átnevezés, nincs részleges írás |
| Durable Object | skálázódik, mint a többi | objektumonként egyetlen példány, sorosított hozzáférés |
Ezek nem hibáznak, hanem szó nélkül nem cache-elnek, ami sokkal nehezebben észrevehető:
GET — minden más metódus egyszerűen kimarad.Set-Cookie fejlécű választ soha nem cache-el, mert az egyedi adatra utalhat. Egy Nuxt-appnál ez azt jelenti, hogy amint a session-middleware ráteszi a sütit a válaszra, a cache-elés csendben megszűnik.Vary: * és a 206 Partial Content válaszok szintén kimaradnak..workers.dev domaineken. A preview URL-ek pedig pontosan ilyenek. Vagyis a cache-viselkedésedet a preview URL-en nem tudod validálni — ott másképp fog viselkedni, mint az egyedi domaineden. Ha a cache-stratégia a kérdés, azt csak custom domainen, éles vagy staging környezetben tudod megnézni.Node-ban megszokott, hogy a válasz elküldése után a folyamat még él, és csinálhatsz utómunkát. Workers-en a futás a válasz után bármikor leállhat — hacsak nem jelzed, hogy még kell idő.
export default defineEventHandler(async (event) => {
const rendeles = await rendelestLetrehoz(event)
// ROSSZ: a válasz után ez talán lefut, talán nem
analitikaKuld(rendeles)
// JÓ: megkéred a futásidőt, hogy várja meg
event.context.cloudflare.context.waitUntil(analitikaKuld(rendeles))
return rendeles
})
| Minta Node-ból | Workers-en |
|---|---|
| „tűz-és-felejtsd” hívás a válasz után | waitUntil nélkül elveszhet |
setInterval ütemezésre | nem tartja életben a Workert → Cron Trigger |
| hosszú háttérfeldolgozás a kérésben | CPU-limit → Queues vagy Workflows |
| memóriabeli munkasor | az isolate eldobásával elvész → Queues |
| kapcsolat nyitva tartása pollinghoz | Durable Object + WebSocket |
waitUntil nem varázsszó: a benne futó munkára is vonatkozik a CPU-limit, és nem alkalmas percekig tartó feldolgozásra. A helyes gondolkodás: waitUntil az ezredmásodperces utómunkára (analitika, log, cache-melegítés), Queues mindenre, ami ennél komolyabb.Ez a modul lényegi haszna: ha valami furcsát látsz, itt keresd meg.
| Tünet | Gyökérok | Hol olvass róla |
|---|---|---|
| Néha más felhasználó adatát látom | ② isolate | 6. modul |
| „Néha lassú”, szórványosan | ② hidegindítás | 15. modul |
| Az időmérésem 0-t ad | ③ óra | 19.4 |
| A kérés megszakad, nincs hibaüzenet | ③ pörgő várakozás → ④ CPU | 19.4 + 19.5 |
EvalError: Code generation… | ① nincs eval | 19.2 |
| Egy könyvtár egy dátumemelés óta romlott el | ① stub-modul | 19.2 |
| Deploy: „Script too large” | ④ bundle-limit | 15. modul |
| Deploy: „startup exceeded” | ② + ④ indulási keret | 15. modul |
| Írok a KV-be, visszaolvasva régi érték | ⑤ eventual consistency | 18.10 |
| A cache lokálisan megy, preview URL-en nem | ⑤ workers.dev korlát | 19.6 |
| A cache-elésem „egyszer csak abbahagyta” | ⑤ Set-Cookie | 19.6 |
| Az analitikám hiányos | ⑥ nincs waitUntil | 19.7 |
10 párhuzamos fetch lassabb, mint várnám | ④ 6 kapcsolat | 19.5 |
| Az ideiglenes fájlom eltűnt | ① /tmp kérésenkénti | 19.2 |
| Egy oldalon nem fut a middleware | előrenderelt → asset | 17.2 |
| Chunk-404 deploy után/közben | globális deploy / verziószórás | 16.9 + 17.5 |
| Nem megy | Miért | Helyette |
|---|---|---|
| AJV és a rá épülő validátorok | sémából kódot fordít → EvalError | Zod, Valibot (8. modul) |
natív bcrypt | C++ addon | node:crypto scrypt, vagy bcryptjs (10. modul) |
sharp és a képfeldolgozók | natív bináris | Cloudflare Image Transformations (13. modul) |
| futásidőben fordító sablonmotorok | new Function | előfordítás build-időben |
worker_threads-re épülő párhuzamosítás | stub — importálható, nem működik | Queues, vagy soros feldolgozás |
| fájlba író loggerek (Winston-transzportok) | nincs valódi lemez | strukturált console.log (16. modul) |
node-cron és setInterval-ütemezők | a Worker nem él a kérések között | Cron Triggers |
| Puppeteer/Playwright közvetlenül | nincs helyi böngésző-folyamat | Browser Rendering binding |
| Redis-kliensek nyers TCP-vel | nincs net-szintű socket a megszokott módon | KV, Durable Object, vagy HTTP-s Redis |
package.json-jában a "engines": { "node": … }-t és a binding.gyp-ot, nézd meg, van-e "browser" vagy "workerd" exportfeltétele, és grep-elj rá az eval, new Function, child_process, fs.watch kifejezésekre. Öt perc, és megspórol egy fél napot.Igazságtalan lenne kilenc szakasznyi korláttal zárni, mert a mérleg másik oldala is valós — és a döntés, amit az 1. modulban meghoztál, ezekért történt:
const t0 = Date.now(); nagySzamitas(); console.log(Date.now() - t0)
Mert a Date.now() a legutóbbi I/O idejét adja vissza, és kódfutás közben nem halad előre — ez szándékos Spectre-védelem. I/O nélkül a két hívás ugyanazt az értéket adja.
const t = Date.now() + 100; while (Date.now() < t) {}?
Végtelen ciklus, mert az óra nem lép előre a szinkron futás alatt — a feltétel sosem válik hamissá. A kérés a CPU-limit túllépésével szakad meg. Helyette: await new Promise(r => setTimeout(r, 100)), mert az await I/O, tehát az óra közben lép.
compatibility_date-et, és elromlott. Mi a valószínű ok?
Egy korábban hiányzó node: modul stubként elérhetővé vált. A könyvtár képesség-felderítése (try { require('worker_threads') } catch {}) most sikeresnek látja az importot, és a párhuzamos ágra megy, ami a stubon elhasal — mélyebben és homályosabb hibával, mint korábban a tiszta catch.
/tmp-be az első kérésben, és a másodikban olvasnád. Mi lesz?
Nem lesz ott. A /tmp tartalma kérésenként külön, nem marad meg kérések között, és párhuzamos kérésekből sem látszik. Az fs Workers-en egy memóriabeli virtuális fájlrendszer, nem a lemezed.
Promise.all-lal. Miért nem tízszer gyorsabb, mint sorosan?
Mert egyszerre legfeljebb 6 kimenő kapcsolat lehet nyitva; a többi vár, amíg felszabadul egy. Tíz forrás egy oldalhoz architekturális jelzés: kell egy aggregáló végpont vagy cache-réteg.
Valami Set-Cookie fejlécet tett a válaszra — tipikusan a session-middleware. A Cache API soha nem cache-el Set-Cookie-t tartalmazó választ, és ezt csendben teszi, hibaüzenet nélkül.
Mert a Cache API nem működik .workers.dev domaineken, a preview URL-ek pedig pontosan ilyenek. A cache-viselkedést csak custom domainen (staging vagy éles) tudod megnézni.
setInterval-alapú ütemezőt és a hosszú háttérfeldolgozást?
Cron Triggers az ütemezést, Queues (nagyobb munkára Workflows) a háttérfeldolgozást. A waitUntil csak ezredmásodperces utómunkára való — analitika, log, cache-melegítés —, mert rá is vonatkozik a CPU-limit.
init(apiKey)-t, aztán globálisan használható. Mi a gond?
A modul-szintű, hitelesített példány az isolate-újrahasználat miatt kérések között megosztott lesz. Node-ban ez helyes (egy folyamat, egy kontextus), Workers-en szivárgás. Hozd létre kérésenként, vagy győződj meg róla, hogy a példány valóban állapotmentes.
Bármelyik kettő: a CPU-limit kizárja a szinkron blokkoló kódot; a bundle-limit fékezi a függőségfa hízását; az isolate-modell megakadályozza a globális állapotra építést; a subrequest-limit korán jelzi, ha egy oldal túl sok forrásból dolgozik. Mind jó gyakorlat Node-ban is — csak ott semmi nem kényszerít rájuk.