nodejs

Stars and mentions

Last 30 days, Oct 4 – Nov 2

GitHub stars1,460
mentions143
065130195260Oct 4Oct 18Nov 2
UTC days · repositories nodejs owns14 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-18

crates.io—
npm50.1B
PyPI—
012.5B25B37.6B50.1BOct 4Oct 18Nov 2
Oct 29crates.io: 0/26 packages observed · npm: 4/17 packages observed · PyPI: 0/1 packages observed

Across 44 known packages in repositories nodejs owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete. PyPI excludes known mirrors.

Top repositories

nodejs’s repositories ranked by new forks, as on trending.

Loading

Contributes to

Repositories outside nodejs’s account where nodejs opened or reviewed pull requests in the past 365 days.

Who noticed nodejs

Everyone who starred or forked nodejs’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

148posts
Nov 37 posts
  1. That JS supports await isn't nearly as much a problem as the fact that it defensively defers execution any time it encounters one. This hurts most badly for iterators. Files are usually read from disks in chunks of 64K bytes. If you present the data as an async byte iterator you'll get 65,534 defensive deferrals, and one actual wait. If you have ten layers of transformation it will be 655,350 defensive deferrals and 10 real waits. These defensive deferrals aren't so expensive in terms of CPU time, but they ARE very expensive in terms of wall clock time. All other work must be halted so the event loop code can run. Eventually synthetic deferrals dominate the wall clock cost of running the program. This isn't hypothetical either. Here's someone trying to build steam parsing on Node and finding that it's too slow to be useful: node#31979 The solution is stream iterators. They are sync when they can be and async when they must be: docs.bablr.org

  2. My Pool Has 4 Threads, DNS Gets 2 In Node, the word "asynchronous" does not describe a single thing. Some calls really are handed to the kernel and come back when the operating system is ready; others are placed on one of four background threads and wait their turn. The second kind is libuv's work pool, its size is four, and that number appears in Node's documentation in a single aside: "its current default value". What led me to that number was my cover generator. In batch mode generate-hero.ts drains its queue with four workers — a hard-coded 4 inside the script — and each worker is one sharp call. In a separate run, in a separate process, my social posting script names nine external hosts in its source. Both are "asynchronous", both sit on their own four threads, and I had no idea how much of those four either one was holding. I wanted to measure this properly, once. The result was sharper than I expected: dns.lookup can never take more than two of those four threads, and neither libuv's documentation nor Node's mentions that limit. Who lands in the pool Node's command-line documentation spells out the list: all fs calls except the file watchers and the explicitly synchronous ones, asynchronous crypto calls such as crypto.pbkdf2, scrypt, randomBytes and generateKeyPair, all zlib calls except the explicitly synchronous ones, and dns.lookup. The warning that follows is fair: because the pool has a fixed size, if one of these takes a long time, other "seemingly unrelated" ones get slower. The sneakiest member of that list is dns.lookup, because you are usually not the one deciding to call it. In Node's lib/net.js, the connect path says it in one line: const lookup = options.lookup || dns.lookup;. So net.connect, http, https and fetch — undici, which sits underneath fetch, uses net.connect for plain HTTP and tls.connect for TLS, and both pass through that same line — resolve names through dns.lookup unless you supply your own resolver. The first step of every outbound HTTP request queues on the same four threads as your file reads. The dns.resolve* family is not on that list. Node's documentation draws the line clearly: those functions do not use getaddrinfo(3), they always query over the network, and they do not use libuv's threadpool. Keep that distinction in mind; we will come back to it with a measurement at the end. The lab: my own resolver, a fixed 300 ms Measuring against a real DNS server is pointless; if the latency moves, you cannot read the table. So I wrote a 49-line UDP server: it holds every incoming query for exactly 300 ms, then answers 127.0.0.2, sets TTL zero (no caching), and most importantly it counts how many distinct names are in flight at the same time. Instead of guessing concurrency from the client side, I let the resolver count it. The lab ran in a node:26-alpine container: Alpine 3.24, Node v26.13.0, libuv 1.52.1, aarch64, 14 cores. /etc/resolv.conf has one line: `nameserver 127.0.0.1. The client calls dns.lookup` for 24 distinct names at once, with family: 4, so each name sends a single A query and the resolver's A/AAAA behaviour stays out of the table. The numbers below are medians of three runs. Alpine uses musl; since the limit measured here lives in libuv, the resolver library does not change the result, but the nsswitch.conf argument later in this article applies to glibc setups. | UV_THREADPOOL_SIZE | Concurrent queries counted by the resolver | 24 names, total | |---|---|---| | unset (4) | 2 | 3,695 ms | | 1 | 1 | 7,414 ms | | 2 | 1 | 7,427 ms | | 3 | 2 | 3,703 ms | | 4 | 2 | 3,712 ms | | 6 | 3 | 2,480 ms | | 8 | 4 | 1,859 ms | | 16 | 8 | 934 ms | The second column is exactly half the pool size, rounded up. What amuses me most in this table is the relationship between the second and third rows. Going from one thread to two moves the total for 24 names from 7,414 ms to 7,427 ms — that is, nowhere. I doubled the threads and what I gained on the DNS side is smaller than the measurement noise. But going from two to three cuts the time in half. Adding one thread buys nothing once and a factor of two the next time. The reason sits in libuv's source, on line 46 of src/threadpool.c: static unsigned int slow_work_thread_threshold(void) { return (nthreads + 1) / 2; } libuv sorts work into three kinds: UV__WORK_CPU, UV__WORK_FAST_IO and UV__WORK_SLOW_IO. src/unix/getaddrinfo.c submits its request as UV__WORK_SLOW_IO; src/unix/fs.c submits every file operation as UV__WORK_FAST_IO, and user code calling uv_queue_work submits as UV__WORK_CPU. Slow-I/O work piles up in a separate queue and only a single "there is slow work" marker goes into the main queue. When a thread picks up that marker and starts running slow work, a counter goes up; if the counter has reached the threshold, the thread pushes the marker back to the tail of the queue and looks for other work. !Diagram So this is not an accident but a deliberate guard: it exists so a slow name lookup cannot swallow the whole pool and block file reads. I have no objection to the logic. My objection is that the limit is written down nowhere. libuv's own documentation covers the default, the absolute maximum, and that the pool is global and shared across all loops; for its scope it says "all file system operations, as well as getaddrinfo and getnameinfo requests". Not one sentence about the half-pool threshold. Node's UV_THREADPOOL_SIZE section does not have it either: there the advice is simply to set a value greater than 4. Follow that advice and DNS can use at most half of whatever you set. Write 8 and the ceiling on your name-resolution concurrency is 4. It is a ceiling, not an allocation: when there is no DNS work those slots do not sit idle, they take other work. Who starves whom The direction of the threshold matters, because it is asymmetric. Two scenarios, both with the pool at its default four threads: A) With 32 pending dns.lookup calls (each answered by my 300 ms resolver) I squeezed in one fs.readFile and one pbkdf2. The fs.readFile waited 0 ms, the pbkdf2 2 ms. Thirty-two names are queued and the file read goes straight through. The guard works. B) With 32 pending pbkdf2 calls — a computation that takes 208 ms on its own — I squeezed in one dns.lookup and one fs.readFile. The fs.readFile waited 1,719 ms, the dns.lookup 2,018 ms. The arithmetic of that second line lands exactly: 32 × 208 ms on four threads is a 1,664 ms queue, and the measurement is 1,719 ms. DNS's 2,018 ms comes from the same place — 1,719 ms of queue plus my resolver's fixed 300 ms, totalling 2,019 ms. The resolver answers in 300 ms while dns.lookup takes two seconds, and 85 percent of that has nothing to do with resolving a name. The lesson here runs the other way: some investigations that start with "our DNS is slow" are looking in the wrong place. What fills the pool is not name resolution but the crypto, compression or image work in the same process. The slow-I/O threshold stops DNS from choking everyone else; nothing stops everyone else from choking DNS. Where the threshold sits on my own pipeline The same question goes to my cover generator, since that is where it started. came from. generate-hero.ts makes a single sharp call: it rasterises the SVG at 200 dpi, fits it to 1200×630 and writes a PNG with compressionLevel: 9. On my Mac (Node v22.16.0, libuv 1.49.2, sharp 0.35.5, libvips 8.18.7, 14 cores) that call takes 75 ms on its own. On an idle pool an fs.readFile takes 0.2 ms. I started the same call N times concurrently and squeezed in a read of a real article file: | Concurrent covers | How long fs.readFile waited | |---|---| | 1 | 0 ms | | 2 | 0 ms | | 3 | 0 ms | | 4 | 9 ms (three runs: 1 – 31 ms) | | 8 | 62 ms (three runs: 56 – 69 ms) | The threshold is exactly at four. Up to three there is still a free thread in the pool, so the read never waits; the moment a fourth cover enters, it starts waiting. I will not lean on the 9 ms in the fourth row — across three runs it wandered between 1 and 31 ms, because the instant a thread frees up is a matter of chance. At eight the number is steady: about 60 ms. Something caught my eye here. My script does not only write two PNGs per article and exit; in --all mode it drains the queue with four workers, and that four is a constant I put there myself: Array.from({ length: 4 }, ...). The comment next to it says it "gains parallel I/O". So when I rebuild the covers for all 2,772 posts I fill the entire pool, and the fourth row of the table is exactly that point. When I wrote that 4 I did not know the pool was also four; the two numbers matching is a coincidence, what follows from it is not — during an --all run every fs read and every dns.lookup in the same process queues behind it. The absolute values are small, I know. What interests me is not the absolute value but that the threshold is visible at all. And one more thing: there is not a single line in my repository that mentions UV_THREADPOOL_SIZE. Not in the scripts, not in the systemd units, not in the workflows. I have been running on four threads and until today I had not chosen that deliberately. When exactly is the pool born Node's documentation says that setting process.env.UV_THREADPOOL_SIZE "is not guaranteed to work" and gives a reason: the pool would have been created "as part of the runtime initialisation much before user code is run". The warning itself is right; the reason does not hold on the Node in this lab. libuv's pool is lazy. The first line of uv__work_submit is `uv_once(&once, init_once)` — the pool is born when the first piece of work is submitted. We can count it from outside, because since libuv 1.50.0 the threads are named libuv-worker: # process is up but nothing has hit the pool yet $ ls /proc/<pid>/task/*/comm | xargs cat | grep -c libuv-worker 0 After a single dns.lookup, that same count returns whatever UV_THREADPOOL_SIZE says — 4 if you wrote 4, 128 if you wrote 128, for one request. That is precisely libuv's sentence about preallocating the maximum number of threads allowed. Five rows, same lab, on where the value is set: | Where it was set | Concurrency counted by the resolver | 24 names | |---|---|---| | nowhere (default 4) | 2 | 3,695 ms | | first line of the JS: process.env... = '16' | 8 | 930 ms | | the same line after one fs.readFile | 2 | 3,720 ms | | UV_THREADPOOL_SIZE=16 in the shell | 8 | 923 ms | | node --env-file=uv.env | 8 | 934 ms | The third row vindicates the documentation's warning, but for a different reason: the setting is not missed because the runtime built the pool early; it is missed because someone submitted work to the pool before you did. A library that reads a file with top-level await, or a dependency that calls randomBytes, quietly makes your setting irrelevant. So the advice stands: do not set this value from inside your code. The two honest paths in the table above are the shell environment and --env-file; both land before the first job. The bill for growing it The ceiling is 1024 (libuv 1.30.0 raised it from 128) and each thread wants an 8 MiB stack (since 1.45.0; before that it was the platform default). Readings from the container's /proc/<pid>/status after a single dns.lookup: | UV_THREADPOOL_SIZE | libuv-worker | VmSize | VmRSS | Total threads | |---|---|---|---|---| | unset (4) | 4 | 848 MiB | 61 MiB | 11 | | 8 | 8 | 880 MiB | 61 MiB | 15 | | 128 | 128 | 1,841 MiB | 61 MiB | 135 | | 1024 | 1024 | 9,021 MiB | 75 MiB | 1,031 | Going from four to eight adds 32 MiB to VmSize: four threads × 8 MiB, exact. At 1024 the virtual size reaches 9 GiB while resident memory stays at 75 MiB — the stacks are paged lazily, just as libuv's documentation says. So the thing to worry about is not RSS; it is virtual address space in an environment capped by ulimit -v, and a scheduler that now has 1,031 threads to deal with. My decision: for a process like my blog pipeline, which writes a couple of images per article and talks to five platforms, 16 is more than enough. Values in the hundreds only make sense in a process that genuinely makes hundreds of concurrent slow calls, and even there the real question is not the pool size but why those calls are concurrent. How to leave the limit behind entirely The cleanest exit is not to grow the pool but not to use it. dns.resolve4 runs on c-ares; the query happens on the event loop, not in the pool. I asked the same 24 names of the same 300 ms resolver, with the default four-thread pool: | Method | Concurrent | 24 names | |---|---|---| | dns.lookup | 2 | 3,695 ms | | dns.resolve4 | 24 | 312 ms | All twenty-four in flight, and the total is the duration of one query. More than an eleven-fold difference, and I never touched the pool. It is not free, of course. Node's documentation states the difference honestly: dns.resolve* does not read /etc/hosts, and it does not use nsswitch.conf and resolv.conf the way dns.lookup does. So there is no guarantee it gives the same answer as ping. In a container, service names written into /etc/hosts, mDNS, search domains — all of that disappears on this path. The middle ground the documentation points to is more sensible: resolve the name with dns.resolve() and use the address, or replace the lookup option that APIs such as socket.connect() and dgram.createSocket() accept with your own implementation — which is also exactly where a cache belongs. Questions to ask about your own setup - How many distinct hosts does your process talk to? Each one means a default dns.lookup, and they all share at most two threads. - Does the same process do image processing, zlib, pbkdf2 or heavy file reads? If so, what fills the pool is probably not DNS but those. - Is UV_THREADPOOL_SIZE set from inside the code? If you cannot prove no work hit the pool before that line, the setting may be ignored. - Do you know how many threads your pool opened with? On Linux, counting libuv-worker in /proc/<pid>/task/*/comm takes two seconds (libuv 1.50.0 and above); on macOS you need sample or vmmap. - If you have a slow-resolver problem, should the answer be dns.resolve? Yes if you have no dependency on /etc/hosts and nsswitch; otherwise your own lookup. - If your traffic goes through global fetch, how do you hand it a lookup? fetch takes no such option; the path runs through undici's dispatcher — setGlobalDispatcher(new Agent({ connect: { lookup } })). - You opened eight worker_threads and assume you have eight pools? The thread count and the queues in threadpool.c are static: there is one pool per process. Eight workers share the same four threads and the same two DNS slots. - What happens when your nameserver dies? A getaddrinfo that has already started cannot be cancelled — uv__work_cancel can only pull work that has not begun off the queue — so two threads stay pinned for as long as `timeout × attempts in resolv.conf` says. Limits without counters Not one measurement in this article produces an alert. There is no counter that goes up when the pool fills. The tooling is not entirely absent: perf_hooks has a 'dns' entry type that tells you how long dns.lookup took, and the gap between the node.threadpoolwork.async and .sync traces gives you the time spent queueing. But the first one does not tell you why it took that long, and the second is never emitted by the pool's two busiest customers — dns.lookup and fs — because neither goes through the code that emits it. The limit is there; in practice the only evidence is in your p99. I ran into the same shape in the [CPU quota article](mustafaerbay.com.tr): a process using seventeen percent of its quota, throttled half a million times in ten days, invisible to every dashboard that looks at averages. In the [keepalive article](mustafaerbay.com.tr) the setting was right but almost no application read it. Here is a third variant: the setting is documented, it is recommended, and half of the recommendation is lost on the way — and the place it is lost is written on line 46 of the source. Two threads out of four. If you did not choose that deliberately, libuv chose it for you. Official Sources - Node.js — UV_THREADPOOL_SIZE (CLI documentation) - Node.js — DNS: Implementation considerations - libuv — src/threadpool.c (v1.x branch) - libuv — Thread pool work scheduling documentation - libuv — src/unix/getaddrinfo.c (v1.x branch) - Node.js — lib/net.js (main branch)

    5 reactions
  3. I Locked Node Into One Directory — Five Doors Stayed Open A single Node process serves this blog. Astro's SSR output, server/entry.mjs, comes up inside a container and reads posts out of a SQLite file. So there is a long-lived process on that machine that can read files and accepts HTTP requests from outside. One evening, while reading the deploy script, a question occurred to me: if that process ever tried to read a file it has no business reading, what would stop it? The answer was "nothing". Whatever the container's root filesystem offers, Node can read. Since Node 22.13 there has been an official tool for exactly this: the permission model. You turn it on with --permission, you say which paths may be read with --allow-fs-read, and everything else comes back as ERR_ACCESS_DENIED. The docs mark it Stability 2 — Stable. So I sat down and tried it. Not to see whether it works — I expected it to work. My question was different: how many ways are there around the wall? Eight routes, across four Node versions. On the v22.16.0 on my own machine six of them worked; on today's stable release, v26.13.0, five still do. Building the wall The lab is two directories: one allowed, one not. I also dropped a symbolic link into the allowed directory — a shortcut pointing at the forbidden file. $ LAB=$HOME/izin-lab $ mkdir -p $LAB/izinli $LAB/yasakli $ echo ISARET_IZINLI_42 > $LAB/izinli/gizli.txt $ echo 'export const x = 1;' > $LAB/izinli/kod.js $ echo ISARET_YASAK_99 > $LAB/yasakli/kasa.txt $ echo 'LAB_ISARET=env_dosyasindan_geldi' > $LAB/yasakli/.env $ ln -s $LAB/yasakli/kasa.txt $LAB/izinli/kisayol.txt First I wanted to see the wall actually hold. One grant: the izinli directory. $ node --permission --allow-fs-read=$LAB/izinli -e '...' ~/izin-lab/izinli/gizli.txt -> ISARET_IZINLI_42 ~/izin-lab/yasakli/kasa.txt -> ERR_ACCESS_DENIED The wall holds. The error object is generous, too — which permission scope, which resource: code: 'ERR_ACCESS_DENIED', permission: 'FileSystemRead', resource: '/Users/mustafaerbay/izin-lab/yasakli/kasa.txt' Read and write are separate scopes; a read grant buys you no write access: $ node --permission --allow-fs-read=$LAB/izinli -e "...writeFileSync(izinli/yeni.txt)" write: ERR_ACCESS_DENIED | FileSystemWrite So far so good. Now let us count the doors. Door 1: a symbolic link points outside Inside the allowed directory sits a shortcut to the forbidden file. Will Node follow it? $ node --permission --allow-fs-read=$LAB/izinli \ -e "console.log(require('fs').readFileSync(process.env.LAB+'/izinli/kisayol.txt','utf8'))" ISARET_YASAK_99 It will. This is not a hole, it is documented behaviour — the "Limitations and Known Issues" section of Node's permission model documentation says plainly that symbolic links are followed even to locations outside the set of granted paths. The docs add a warning: make sure none of the paths you grant contain relative symbolic links. In practice that means the directory you pass to --allow-fs-read is now your responsibility. Any process that can write into it — and node_modules is the most crowded example of such a directory — can drop a shortcut there and have the outside read for you. An unpleasant sentence for people like me, who typed --allow-fs-read=<repo root> and relaxed. Door 2: everything after the wildcard is ignored The documentation supports wildcards. Out of plain shell habit, I wanted to say "read only the JavaScript files": $ node --permission --allow-fs-read="$LAB/izinli/*.js" \ -e "console.log(require('fs').readFileSync(process.env.LAB+'/izinli/gizli.txt','utf8'))" ISARET_IZINLI_42 It read the .txt file. Because * is not a shell pattern here: in the docs' own words, after a wildcard character all subsequent characters are ignored. So /home/*.js behaves like /home/*. The grant I wrote as izinli/*.js was applied as izinli/*. This is the quietest way to over-grant by typo. No error, exit code zero, no log line. Just a door wider than you intended. Filtering by extension is not the permission model's job — do it in your application layer if you like, but you cannot do it on the grant line. A second detail: when you grant an existing directory, Node appends a * automatically; for a directory that does not exist, it does not. The measurement: $ node --permission --allow-fs-read=$LAB/yokdizin -e '...has("fs.read", LAB+"/yokdizin/x")' yokdizin/x: false $ node --permission --allow-fs-read=$LAB/yokdizin/'*' -e '...' with wildcard, yokdizin/x: true If you are granting an output directory that has not been created yet, write the wildcard yourself. Door 3: --env-file reads before the permission model exists This blog's server has an /etc/mustafaerbay.env with API keys in it. The other day, when I [handed the same .env file to five parsers](mustafaerbay.com.tr), the measurement showed only 4 of its 24 lines producing the same value everywhere. Now I asked the same file a different question: does the permission model police a file read through --env-file? $ node --permission --env-file=$LAB/yasakli/.env \ -e "console.log(process.env.LAB_ISARET)" env_dosyasindan_geldi I granted no --allow-fs-read at all. The file was read anyway. The docs cover this too: flags such as --env-file and --openssl-config are designed to read files before environment initialisation, and are therefore not subject to the permission model's rules. The same goes for V8 flags that can be set at runtime via v8.setFlagsFromString. It makes sense — a file is being read while the permission model does not exist yet. But the consequence stands: having turned the permission model on does not stop a file named on the command line from being read. Whoever composes the command line owns that much of the filesystem. Door 4: node:sqlite walks past the check This was the one that mattered most to me, because this blog's posts are read out of a SQLite file. First I put a small database into the forbidden directory — this step with the permission model off, in an ordinary process: $ node -e "const {DatabaseSync} = require('node:sqlite'); const db = new DatabaseSync('$LAB/yasakli/deneme.db'); db.exec('CREATE TABLE t(x TEXT)'); db.exec(\"INSERT INTO t VALUES('ISARET_SQLITE_77')\"); db.close();" Then I opened that same file from a process whose only grant is the izinli directory: $ node --permission --allow-fs-read=$LAB/izinli -e " const {DatabaseSync} = require('node:sqlite'); const db = new DatabaseSync('$LAB/yasakli/deneme.db', {readOnly:true}); console.log('node:sqlite READ:', db.prepare('SELECT x FROM t').get().x);" node:sqlite READ: ISARET_SQLITE_77 No grant, no obstacle. This is documented as well: the permission model "does not guarantee that users will not be able to access the file system through other means, such as through the node:sqlite module." The reason is simple — the check lives in the node:fs bindings, while SQLite opens the file in its own C++ layer. Where there is no checkpoint, there is no check. I got the same result on Node 24.22.2 and 26.13.0. So this is not an old version's gap; it is the model's boundary. !Diagram The ordering in that diagram is a claim too: the check sits only on the node:fs branch. The other three branches either run before the model is set up or never pass through it. Door 8: a worker thread given execArgv: [] (I am keeping the table's numbering; 5, 6 and 7 are the next section's subject.) Node 22's documentation has a single sentence: "The model does not inherit to a worker thread." I read it and concluded workers run free. Measuring showed the opposite. Note that both runs below do pass --allow-worker — without it, constructing the Worker would already be refused in the WorkerThreads scope and the thing being measured would not be a file read at all: $ node --permission --allow-fs-read=$LAB/izinli --allow-worker \ -e "new (require('worker_threads').Worker)('$LAB/izinli/oku.cjs')" (node:75778) SecurityWarning: The flag --allow-worker must be used with extreme caution. ... (node:75778) SecurityWarning: The flag --allow-worker must be used with extreme caution. ... CLOSED ERR_ACCESS_DENIED $ node --permission --allow-fs-read=$LAB/izinli --allow-worker \ -e "new (require('worker_threads').Worker)('$LAB/izinli/oku.cjs',{execArgv:[]})" (node:75780) SecurityWarning: The flag --allow-worker must be used with extreme caution. ... OPEN ISARET_YASAK_99 The default Worker was blocked. The one that was not blocked is the one I opened with execArgv: []. The reason: a default worker inherits the parent's CLI flags, --permission and the --allow-* set included. When you pass execArgv explicitly — the empty array counts — that list replaces the inherited flags, and the grants disappear with them. The proof is sitting in the output already: the first run printed the SecurityWarning twice, the second once. In the first case the worker that inherited the flags re-emits the warning on its own account; the worker given execArgv: [] never received those flags, so it stays quiet. You do not even need to call process.permission.has() to tell whether inheritance happened. Current Node documentation now spells this distinction out and adds that the difference is intended, not a bypass. That single sentence in the Node 22 docs, on its own, points the reader the wrong way; without measuring I would have stayed on the wrong side of it. When a doc's short sentence and the code's behaviour disagree, the code wins. The door that closed: spawn versus fork This was the most interesting result. On my own machine, Node v22.16.0, the parent process could not read the forbidden file but its child could: == v22.16.0 == 5 spawn OPEN 6 fork CLOSED ERR_ACCESS_DENIED Same parent, same file, two different APIs, opposite outcomes. fork() copies the parent's execArgv to the child — so --permission travels with it and the child is born inside the jail. spawn() was not copying it: the child started as if the permission model had never been switched on. Node prints an honest warning while this happens: SecurityWarning: The flag --allow-child-process must be used with extreme caution. It could invalidate the permission model. I ran the same script on three more versions — two in containers (Linux), the last one on my own machine again, with the current LTS downloaded from nodejs.org: | Route | v22.16.0 | v22.23.4 | v24.22.2 | v26.13.0 | |---|---|---|---|---| | 0 direct read | closed | closed | closed | closed | | 1 symbolic link | open | open | open | open | | 2 wildcard truncation | open | open | open | open | | 3 --env-file | open | open | open | open | | 4 node:sqlite | open | open | open | open | | 5 spawn | open | closed | closed | closed | | 6 fork | closed | closed | closed | closed | | 7 worker | closed | closed | closed | closed | | 8 worker execArgv:[] | open | open | open | open | One thing to know while reading that table: 22.16 and 26.13 ran on macOS, 22.23 and 24.22 in Linux containers. In an article about symbolic links and path grants that is not a trivial detail — but the only difference between the columns is the spawn row, and the cause of that difference is not the operating system, it is the patch below. v26.13.0 is the newest stable release nodejs.org listed on the day I wrote this. The closing point is on record: the patch titled "permission: propagate permission model flags on spawn" landed in Node 22.18.0. Its own description states exactly what the lab showed — previously only child_process.fork propagated the exec arguments to the child process. Now spawn and spawnSync do too, not on the command line but through the NODE_OPTIONS environment variable. The 22.16 on my machine was two patch releases short of it. The lesson here is about reading versions. "We're on Node 22" maps to two different rows of that table. Security behaviour changed inside a single major line. One more note: none of these experiments ran on Node 20 at all. == v20.20.2 == 0 direct read node: bad option: --permission On Node 20 the flag is called --experimental-permission. The new name became stable in v22.13; the old name is still accepted as an alias across the 22 line. But the trap is symmetric, and that went through the lab too: $ docker run --rm node:22-alpine node --experimental-permission ... accepted $ docker run --rm node:24-alpine node --experimental-permission ... node: bad option: --experimental-permission The old name was removed in Node 24. So it is a minefield in both directions: write --permission and the process never starts on a Node 20 image; write --experimental-permission and it never starts on a Node 24 one. Both die with "bad option", and both die before any of your application's code runs — which at least makes it a loud failure rather than a quiet one. The comma trap: I wrote two directories, I got zero I did not go looking for this one; I fell into it. Wanting to allow two directories, I reached for a comma out of habit from other command-line tools: $ node --permission --allow-fs-read="$LAB/izinli,$LAB/yasakli" \ -e "console.log('izinli:',process.permission.has('fs.read','$LAB/izinli/gizli.txt'))" izinli: false (node:75172) Warning: The --allow-fs-read CLI flag has changed. Passing a comma-separated list of paths is no longer valid. Documentation can be found at https://nodejs.org/api/permissions.html#file-system-permissions exit code: 0 Neither. The comma-separated list counts as a single path name, and since nothing exists under that name, no grant is created at all. Here I have to own a measurement mistake of my own. The first time I ran this line I was filtering the output through grep -vi warning — a filter I had added to drop ExperimentalWarning noise from the container runs. That filter swallowed the warning above, and for a while I walked around believing Node says nothing at all. Node does say something, and says it well: that the flag's behaviour changed, with a link to the documentation. The comma form was invalidated back in v20.7.0. The trap is not silence, it is severity: this is a Warning, not an Error. The process starts, the exit code is zero, the application runs — you simply have no permissions anywhere. For someone filtering warnings out of CI output, which is exactly what I was doing, it is easy to miss. The correct form is to repeat the flag: $ node --permission --allow-fs-read="$LAB/izinli" --allow-fs-read="$LAB/yasakli" -e '...' izinli: true | yasakli: true The difference between those two lines could be a systemd unit you believe you have hardened granting nothing at all, or your application dying on its first read. After writing your grants, verify them once with process.permission.has(); it is a one-liner: $ node --permission --allow-fs-read=/opt/app \ -e "console.log(process.permission.has('fs.read','/opt/app/dist/x'))" true I put my own validator in jail The lab is all very well, but the real question was what happens in production. This repository has a 126-line script, scripts/validate-content-sources.mjs, that checks every article's source list. I tried running it under the permission model and raised the grants one at a time. $ node --permission scripts/validate-content-sources.mjs --staged permission: 'FileSystemRead', resource: '.../scripts/validate-content-sources.mjs' $ node --permission --allow-fs-read="$PWD" scripts/validate-content-sources.mjs --staged permission: 'ChildProcess', resource: '' $ node --permission --allow-fs-read="$PWD" --allow-child-process scripts/validate-content-sources.mjs --staged Kaynak dogrulamasi: staged icerik yok. # "source check: nothing staged" On the first step it could not read the script itself. On the second it stopped in the ChildProcess scope, because the script calls git diff --cached through execFileSync. That first step was leading me to a false generalisation: "under the permission model you have to grant even your application's own entry file." Before writing it down I ran the same thing on the other versions, and the generalisation collapsed: $ node --permission ~/izin-lab/giris.cjs # v22.16.0 Error: Access to this API has been restricted... $ docker run ... node:22-alpine node --permission /s/giris.cjs entry point ran: v22.23.4 $ docker run ... node:24-alpine node --permission /s/giris.cjs entry point ran: v24.22.2 $ ./node-v26.13.0/bin/node --permission ~/izin-lab/giris.cjs entry point ran: v26.13.0 No grants at all, and three of them ran. The reason sits in the docs, in the version history row for --allow-fs-read: v24.2.0 and v22.17.0 — "Entrypoints of your application are allowed to be read implicitly." So the second piece of evidence for this article's backbone — the patch version is a behaviour version — is inside the same minor line as the first, one patch earlier. 22.17 opened the entry point, 22.18 brought flag propagation on spawn. The 22.16 on my machine predates both, which is why the three-step escalation above reproduces exactly only on that version. On 22.17 and later the first step does not fail and you start from the second. That implicit grant ends in a subtle place. I called the same file, with the same version, through a path whose parent is a symbolic link — on macOS, /tmp is a link to private/tmp: $ ./node-v26.13.0/bin/node --permission /tmp/.../giris.cjs Error: Access to this API has been restricted... at Object.realpathSync (node:fs:3479:37) $ ./node-v26.13.0/bin/node --permission /private/tmp/.../giris.cjs entry point ran: v26.13.0 Same file, same flag, two spellings. The implicit grant given to the entry point does not cover resolving that path to its real one; the realpathSync during resolution wants a grant. Anyone running scripts out of /tmp on macOS will meet this, and it belongs to the "why doesn't this happen on my machine" family. The minimal pair turned out to be: read access to the repository root, plus --allow-child-process. Three runs, same result. But here is the irony: to put a 126-line validator in jail, I have to hand it the grant that — on the Node version on my machine — removes the jail entirely for its children. On a pre-22.18 Node, the protective value of this setup is limited to the parent's own fs calls. The tool chain, not my code, sets the grant list The scripts that render cover images run on TypeScript through tsx. I tried to jail that too — without even giving it work to do, just asking for its version. $ node --permission --allow-fs-read="$R" --allow-fs-read="$NM" node_modules/.bin/tsx --version code: 'ERR_ACCESS_DENIED', permission: 'FileSystemWrite', resource: '/var/folders/nn/fmy2b8m575143sfsvs3wrfz40000gn/T/tsx-501' tsx writes a cache into the operating system's temporary directory. I granted the write, and it stopped in a different scope: $ node --permission ... --allow-fs-read="$T" --allow-fs-write="$T" node_modules/.bin/tsx --version tsx v4.21.0 code: 'ERR_ACCESS_DENIED', permission: 'ChildProcess', resource: '' It printed the version, then asked for a child process and stopped. The full working list came to five grants: repo read, node_modules read, temp directory read, temp directory write, child process. All of that to say tsx --version. That number has nothing to do with my code. The script I wrote reads two files and writes a PNG; the length of the grant list is set by the TypeScript runner's caching strategy and its decision to invoke the compiler in a separate process. If you are going to attach the permission model to your application, what you are constraining is not the code you wrote but the entire stack underneath it. The addon door: courtesy or model? On the blog's SSR side I hit a different wall. Posts are read with better-sqlite3, and that is a native addon: $ node --permission --allow-fs-read="$R" --allow-fs-read="$NM" ... -e "...new Database(...)" require passed addon blocked: ERR_DLOPEN_DISABLED The docs say this as well: under the permission model a process cannot load native addons by default, and without --allow-addons you get ERR_DLOPEN_DISABLED. The require('better-sqlite3') line on its own goes through; the failure happens at the first Database object, the moment the .node file is actually loaded. When I did pass --allow-addons, the attempt to open the forbidden database stopped again — but not because of the permission model: code: ERR_ACCESS_DENIED | resource: /Users/mustafaerbay/izin-lab/yasakli at Object.existsSync (node:fs:292:18) at new Database (.../node_modules/better-sqlite3/lib/database.js:64:57) What stopped it was better-sqlite3's own existsSync call in its JavaScript layer. The library's courtesy saved me, not the model itself. Doing the same work with the core node:sqlite module — Door 4 — nobody stopped it at all. The third-party library got caught because it happened to go through a checked path; the core module went through because it does not. Read that sentence backwards and it says a dependency upgrade can quietly change your level of protection. I did not have to hunt for grants by hand What I did in the previous two sections — run, take the error, add a grant, run again — works, but it is primitive. --permission-audit, which arrived in Node 25.8, is precisely the automated version of it: the checks run, violations are published over node:diagnostics_channel, but access is not denied. $ cat denetim.cjs const dc = require('node:diagnostics_channel'); for (const scope of ['fs', 'child', 'worker']) dc.subscribe('node:permission-model:' + scope, (m) => console.log('violation:', m.permission, '->', m.resource)); require('fs').readFileSync(process.env.SIR, 'utf8'); require('child_process').spawnSync(process.execPath, ['-e', '0']); console.log('script ran to the end'); $ ./node-v26.13.0/bin/node --permission-audit denetim.cjs violation: FileSystemRead -> /Users/mustafaerbay/izin-lab/yasakli/kasa.txt violation: ChildProcess -> .../bin/node script ran to the end The whole shopping list in one run: which scope, which resource. In enforcing mode the same script stops at the first ChildProcess violation and you never see the rest — which is also why my escalation took three rounds. A warning: leaving this on in production is not protection, only measurement. Audit mode blocks nothing. In a real deployment the order should be — first --permission-audit to produce the list, then write the grants, then enforce with --permission. So what is this good for? Looking at the table so far it is easy to say "pointless", but I think that would be wrong. Node's own documentation says what it is not in its very first paragraph: this feature does not protect against malicious code; Node trusts any code it is asked to run. Malicious code can bypass the permission model and execute arbitrary code without its restrictions. So it is not a sandbox, it is a guard rail. When those two get confused, people lean on the wrong thing. After measuring, I wrote down four questions for myself: - What kind of mistake am I protecting against? It helps against a readFileSync that lost its way, a misconfigured backup script, a library that decides it would like to read /etc too. It does not help against an attacker who can run code inside the process. - Which grant am I handing over? --allow-child-process, --allow-addons and --allow-worker lower the wall the moment they are given. Node says so itself: passing each of the four in turn (--allow-wasi included) printed the same SecurityWarning line — "must be used with extreme caution. It could invalidate the permission model." - Who can write into the directory I allowed? The symbolic link door closes on exactly this question. Anyone with write access can bore through your read wall. - On which Node version? The table above shows why this is a separate question. The patch version is a behaviour version. If you want an operating-system-level jail, this is not the place. I previously wrote about [Landlock, where an application confines itself](mustafaerbay.com.tr): there the check lives in the kernel, and neither node:sqlite nor a .node file can walk around the rule. The permission model's place is not to replace a kernel restriction, but to sit as a thin layer above it. What I settled on I am not putting the blog's SSR process under the permission model today. The reason is in the table: posts are read by better-sqlite3, so I would have to pass --allow-addons; once that grant is given part of the protection goes with it, and what I get in return is preventing code I wrote myself from accidentally reading /etc. That trade is already solved more cleanly at the container level — read-only mounts, a separate user, a narrow filesystem. But I am considering it for the one-shot scripts under scripts/. They are short-lived, have few dependencies, mostly use no native addons, and what they will read is known up front. A guard rail is exactly for places like that. And the first step is no longer hunting for grants by hand: one run of --permission-audit, then turn the list into flags. One last note in fairness: eight routes is not an exhaustive list. The docs' own "limitations" section names others I did not try — operations on already-open file descriptors, FFI, OpenSSL loaders, the trace files written by --trace-event-file-pattern. The lab covered only the ones that could plausibly show up in my own setup. I tried eight doors, five are still open, and all five are written down in the documentation. The difference between reading an instrument's limits and seeing them measured turned out to be this: I had skimmed past the sentence "symbolic links will be followed" three times, but I had not noticed what it did to my --allow-fs-read=<repo root> line until ISARET_YASAK_99 appeared on screen. Official Sources - Node.js — Permissions (permission model documentation) - Node.js — Command-line API (--permission and the --allow-* flags) - nodejs/node — "permission: propagate permission model flags on spawn" patch - Node.js v22.x — Permissions (the 22 line's own documentation)

    6 reactions · 1 comment
  4. 🔏 Time to work on Node.js macOS notarization and finally fix the root cause: node#67140

    4 likes
  5. Stop Using Default Node.js fetch() in Production Without These Two Fixes --- description: Native Node.js fetch() looks clean, but its 300-second default timeout and unconsumed error streams can silently exhaust your production sockets and memory. Fix both with a hardened wrapper. --- When native fetch() became stable in Node.js, engineering teams celebrated. We finally had a standard, zero-dependency HTTP client built directly into the runtime, powered by Node's fast undici engine. Thousands of teams ripped out axios and node-fetch and replaced them with clean, standard await fetch(url) calls. Then the production incidents started. If you copy-paste browser-style fetch() calls into a backend Node.js service, two misleading defaults will eventually take your service down under load: a 5-minute default timeout and lazy garbage collection on unconsumed response streams. Let's look at why a 6-line fetch() function that passes every code review can silently starve your production server of sockets and memory—and the exact wrapper that fixes both. --- The Code That Looks Completely Fine (Until Traffic Spikes) Here is how 90% of developers write an outbound API call in Node.js today: // ❌ Looks clean in code review, dangerous in production export async function fetchUserProfile(userId: string) { const response = await fetch(`https://api.partner-service.com/v1/users/${userId}`, { headers: { Authorization: `Bearer ${process.env.API_TOKEN}` }, }); if (!response.ok) { throw new Error(`Partner API failed with status ${response.status}`); } return response.json(); } In local development and staging, this code works flawlessly. In production, when api.partner-service.com experiences a brownout or starts returning 502 Bad Gateway html pages at 200 requests per second, this function triggers a cascading outage in your Node.js process. Here is why. --- Trap #1: The 300-Second (5-Minute) Default Timeout How long does await fetch(url) wait if the upstream server accepts the TCP connection or TLS handshake, and then simply hangs without sending response headers or finishes the body slowly? In Node.js, native fetch() follows the Chromium/Undici default: 300,000 milliseconds (5 minutes) for both headersTimeout and bodyTimeout. {% card %} The 5-Minute Trap: Without an explicit AbortSignal, a single degraded third-party API will hold your Node.js request handler, database transaction, or serverless execution open for up to 300 seconds before throwing a HeadersTimeoutError. {% endcard %} Think about the math on a backend service handling just 50 requests per second: - An upstream webhook or payment provider starts hanging. - None of your fetch() calls fail quickly; instead, 50 new promises pile up every single second. - Within 10 seconds, you have 500 concurrent in-flight requests holding memory closures, open file descriptors, and often database pool connections waiting for fetch() to settle. - Your container hits an Out-Of-Memory (OOM) kill or your upstream load balancer starts dropping health checks long before the 300-second default timeout ever fires. --- Trap #2: Throwing on !response.ok Without Draining the Body Look again at the error branch in our original snippet: if (!response.ok) { // ❌ We throw immediately without reading or canceling response.body! throw new Error(`Partner API failed with status ${response.status}`); } When await fetch() resolves, Node.js has only parsed the HTTP status line and response headers. The underlying TCP socket is still streaming or buffering the response body (for example, an HTML error page from Cloudflare or a verbose JSON error payload from an upstream microservice) through a Web ReadableStream. If you neither consume the body (await response.text() / await response.json()) nor explicitly cancel it (await response.body?.cancel()), what happens to the underlying HTTP/1.1 keep-alive socket in Undici's connection pool? According to the official Undici documentation, the socket cannot be cleanly reused immediately. Instead, Undici must wait for V8's garbage collector to eventually run a finalizer on the unreferenced response object before tearing down or reclaiming the stream. {% details Why V8 Garbage Collection makes unconsumed bodies worse on large servers %} V8's garbage collector is intentionally lazy when a Node.js container has plenty of free RAM. If your container has a 2 GB memory limit and your heap is only at 300 MB, V8 feels no pressure to run a major GC cycle. Meanwhile, if an upstream service starts returning 500 Internal Server Error or 429 Too Many Requests at high volume and your code throws without consuming or canceling response.body: 1. Hundreds of unread ReadableStream buffers sit in memory waiting for GC. 2. Underlying sockets stay occupied or get destroyed instead of returning cleanly to the keep-alive pool. 3. Your service experiences sudden RSS memory climbs and socket exhaustion (UND_ERR_CONNECT_TIMEOUT) just when you need resilience most. {% enddetails %} --- The Fix: Always Enforce a Deadline and Always Drain or Cancel the Stream You don't need to reinstall axios to fix this. Native Node.js provides everything you need via AbortSignal.timeout() and explicit stream disposal. 1. Use AbortSignal.timeout(ms) on Every Call Added natively to Node.js and modern runtimes, AbortSignal.timeout(ms) automatically aborts the fetch() request and rejects with a TimeoutError (DOMException) if the entire request-response cycle (including reading the body!) exceeds your deadline. 2. Consume or Cancel response.body on Non-2xx Responses Whenever response.ok is false, either read the error payload with await response.text() (which both drains the socket for immediate keep-alive reuse and gives you diagnostic context) or call await response.body?.cancel() if you don't care about the error body. Here is a drop-in, production-hardened safeFetchJson wrapper you can use across any Node.js service: // lib/http.ts export interface SafeFetchOptions extends RequestInit { timeoutMs?: number; } export class HttpResponseError extends Error { constructor( public readonly status: number, public readonly url: string, public readonly bodyPreview: string ) { super(`HTTP ${status} from ${url}: ${bodyPreview}`); this.name = 'HttpResponseError'; } } export async function safeFetchJson<T>( url: string, options: SafeFetchOptions = {} ): Promise<T> { const { timeoutMs = 5000, signal: callerSignal, ...fetchOptions } = options; // 1. Combine a strict timeout deadline with any caller-provided AbortSignal const timeoutSignal = AbortSignal.timeout(timeoutMs); const signal = callerSignal ? AbortSignal.any([callerSignal, timeoutSignal]) : timeoutSignal; const response = await fetch(url, { ...fetchOptions, signal, }); // 2. Always drain or cancel the body stream on error statuses if (!response.ok) { let errorPreview = ''; try { const rawText = await response.text(); errorPreview = rawText.slice(0, 256); // Keep logs bounded } catch { await response.body?.cancel().catch(() => {}); } throw new HttpResponseError(response.status, url, errorPreview); } // 3. Consuming .json() drains the stream and releases the keep-alive socket return (await response.json()) as T; } Bonus: What If You Only Need Headers (HEAD or Status Check)? If you intentionally call fetch(url) only to inspect response.status or response.headers.get('content-length') and never plan to read the body, always cancel the body immediately in a finally block: export async function checkEndpointReachable(url: string): Promise<boolean> { const response = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(3000), }); try { return response.ok; } finally { // ✅ Immediately release the underlying Undici socket without waiting for V8 GC await response.body?.cancel().catch(() => {}); } } {% cta undici.nodejs.org %} Read the Official Node.js Undici Garbage Collection and Socket Reuse Guide {% endcta %} --- Production fetch() Resilience Checklist Before deploying native fetch() to production in Node.js, verify these five items: - [ ] Every outbound fetch() call includes signal: AbortSignal.timeout(ms) (typically 2000–8000 ms for synchronous user-facing requests). - [ ] Combine caller cancellation signals with timeouts using AbortSignal.any([callerSignal, AbortSignal.timeout(ms)]). - [ ] Every !response.ok branch either reads await response.text() or calls await response.body?.cancel() before throwing. - [ ] Any fire-and-forget request that only inspects status codes or headers cancels response.body in a finally block. - [ ] Distinguish between err.name === 'TimeoutError' (deadline exceeded) and err.name === 'AbortError' (caller canceled) in your error monitoring logs. --- The Takeaway Native fetch() in Node.js was designed for spec compliance with browsers—not for defensive backend resilience out of the box. In a browser tab, waiting 300 seconds or letting garbage collection clean up an abandoned stream rarely hurts anyone. In a high-throughput Node.js server, those same defaults turn minor upstream hiccups into full service outages. Wrap your fetch() calls once: enforce an AbortSignal.timeout() on every request, and never leave a response stream unconsumed when throwing an error. Have you migrated your Node.js services to native fetch(), or do you still prefer a wrapper library like ky or got for built-in timeouts and retries? --- Sources & Further Reading - Node.js / Undici Documentation: Garbage Collection & Unconsumed Response Bodies - MDN Web Docs: AbortSignal.timeout() Static Method - MDN Web Docs: AbortSignal.any() Static Method - Node.js Official Documentation: Global fetch() API --- Connect with Me If you found this article helpful, let's connect! - 💻 GitHub: johnnylemonny {% user johnnylemonny %}

    18 reactions · 9 comments
  6. YouTube · · YouTube description · Just Known Dev · nodejs/node

    Your GitHub Actions Jobs May Now Run on AWS | Dev News Five developer stories and some updates in about four minutes: what happened, why it matters, and what to do. 1. GitHub Actions: eligible Linux jobs on standard 2- and 4-core runners may now run on AWS as well as Azure (automatic on Free, Pro and Team; Enterprise Cloud opts in). They can use new outbound IPs, so allowlists built on Azure ranges, including Entra Conditional Access, can block them: sync with the actions list from api.github.com/meta. Included Actions usage is now a dollar amount ($12 Free, $18 Pro/Team, $300 Enterprise Cloud a month), and Copilot cloud sandboxes draw from it, with billing from November. macos-14 jobs fail during brownouts Oct 29 and Oct 30, 14:00 to 24:00 UTC; the image retires Nov 2. 2. CISA added Linux kernel CVE-2026-46300 ("Fragnesia", per Red Hat) to its Known Exploited Vulnerabilities list, due Oct 31, saying it can be chained with PHP2Shell to plant web shells and run commands as root. Patch PHP (8.5.12 / 8.4.27 / 8.3.36 / 8.2.35) and the host kernel (fixed upstream in 5.10.257, 5.15.208, 6.1.174, 6.6.141, 6.12.91, 6.18.33, 7.0.10, or your distro's update). PHP2Shell's federal deadline is Oct 29. 3. Jenkins fixed 15 core vulnerabilities (13 High), including a setup-wizard race that lets an unauthenticated attacker create an admin account on a new instance. Upgrade to 2.588 or LTS 2.580.2. 4. Apache Kafka disclosed 12 vulnerabilities. A Kafka 4.1–4.3.0 broker using SASL/OAUTHBEARER with no JWKS endpoint URL (the default) skips signature checks and accepts unsigned tokens (CVE-2026-110985). Kafka Streams apps and consumers using ListSerde can be crashed by a crafted record. Set sasl.oauthbearer.jwks.endpoint.url, then upgrade brokers and client libraries to 4.2.2, 4.3.2 or 4.4.0. 5. PyTorch 2.15 drops Python 3.10, needs NVIDIA driver 580+ and a Turing (sm_75) or newer GPU for the default CUDA wheels, needs macOS 15 for MPS, and writes distributed checkpoints 2.14 can't load. Pin torch. Updates: Node.js 26 'Lithium' became LTS on Oct 28 (26.13.0); the first Node 27 alpha slipped and is expected shortly. Tailscale 1.104.4 fixes its SSH and Serve flaws. On Oct 30, Amazon Pinpoint's console and engagement features end, and Cloud Run functions decommission Node.js 20. ⏱ Chapters 0:00 Intro 0:13 Today's rundown 0:43 GitHub Actions jobs may now run on AWS 1:29 PHP2Shell gets root: a kernel bug joins KEV 1:59 Jenkins fixes 15 core flaws 2:23 Kafka brokers can accept unsigned tokens 2:57 PyTorch 2.15 drops Python 3.10 3:23 Updates 3:48 Sign-off 📰 Sources GitHub Actions multi-cloud runners: github.blog Copilot cloud and the dollar-based Actions pool: github.blog GitHub Docs, included usage: docs.github.com macos-14 retirement: github.blog CISA KEV catalog (feed): cisa.gov CVE-2026-46300 record: cveawg.mitre.org Red Hat on CVE-2026-46300: access.redhat.com Palo Alto advisory: security.paloaltonetworks.com Jenkins Security Advisory 2026-10-28: jenkins.io Apache Kafka CVE list: kafka.apache.org Kafka advisories on oss-security (Oct 28): openwall.com PyTorch 2.15 release notes: github.com/pytorch/pytorch/releases/tag/v2.15.0 Node.js 26.13.0: nodejs.org Node.js 27 alpha release PR: node#67062 Tailscale security bulletins: tailscale.com Amazon Pinpoint end of support: docs.aws.amazon.com Cloud Run functions runtime support: cloud.google.com Just Known Daily: developer news, every day. From unknown to known. A channel by Just Unknown. #technews #github #devops #cybersecurity #pytorch #developers

    56 views
  7. Release took a lot longer than planned today due to a last minute infrastructure issue caused by updating our indexing tool for the 27 alpha: nodejs-dist-indexer#110 It wasn't the 27 alpha changes that were at fault, and more that the existing tests didn't catch the breakage 😞.

    1 favourite
  8. This change makes inflateSync 57-72% and deflateSync 12% faster node#66864

    14 likes · 497 views
  9. yeah, I'm missing Deno.serve the most :-( Maybe one day: node#63096

    1 like
  10. nodejs/release Node.js 26がLTSになった。 これでSeaとかが普通に使える

    78 likes · 20 reposts · 6,711 views
Trending awards, every completed day and week
Gold
151st place finishes
Silver
152nd place finishes
Bronze
83rd place finishes
Awarded repositories
1Owned by nodejs now

Repositories

Ordered by gold, then silver, then bronze.

  1. nodeBest: 1st by stars, 3,724 stars15158

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.