Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

fetch(): with keep-alive and a small response, every call keeps a Web Streams handle id; the process aborts after ~1M calls

Open
#12,862 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
48/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust, typescript

Research direction

Start by reading crates/perry-stdlib/src/streams/idalloc.rs, especially the handle allocation and retirement behavior described by the panic. Build the supplied TypeScript reproduction and compare small versus big responses with the keep-alive server to investigate where handles remain live. Done when the small-response loop completes without unbounded RSS growth or exhausting the handle-id band.

Written by the indexing model from the issue text.

Description

bug

What happened

Sequential fetch() + text() calls to a local HTTP/1.1 keep-alive server that answers a 16-byte body never release something per call. RSS grows in steps (~230 MB, ~410 MB, ~780 MB). After about 1 000 000 calls the process aborts:

thread '<unnamed>' panicked at crates/perry-stdlib/src/streams/idalloc.rs:130:9:
Web Streams handle id band exhausted: 1048576 ids live or quarantined (see PERRY_STREAM_ID_QUARANTINE)

The same loop runs 1 100 000 calls with flat memory when the response is 16 KiB (same keep-alive server), or when the server closes the connection after each response (python3 -m http.server, HTTP/1.0).

What you expected

The loop completes with flat memory, as it does with 16 KiB responses or without keep-alive.

Minimal reproduction

// leak.ts
import { readFileSync } from "node:fs";

const file = process.argv[2] ?? "small";
for (let i = 1; i <= 1100000; i++) {
  const r = await fetch("http://127.0.0.1:9081/" + file);
  await r.text();
  if (i % 100000 === 0) {
    const rss = readFileSync("/proc/self/status", "utf8").split("\n").find((l: string) => l.startsWith("VmRSS:"));
    console.log(file, i, rss);
  }
}
console.log("done");

A keep-alive server (Python standard library; /small returns 16 bytes, anything else 16 KiB):

python3 -c '
import http.server
class H(http.server.BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"
    wbufsize = 65536
    def do_GET(self):
        body = b"x" * (16 if self.path == "/small" else 16384)
        self.send_response(200)
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)
    def log_message(self, *args):
        pass
http.server.ThreadingHTTPServer(("127.0.0.1", 9081), H).serve_forever()
' &

Command you ran:

perry compile leak.ts -o leak
./leak small    # aborts after ~1 000 000 calls (~160 s)
./leak big      # completes

Environment

  • Perry version: 0.5.1661 (main at c9573408b, 2026-10-18, built from source)
  • Host OS: Debian 13, Linux 6.12, x86-64
  • Target: native
  • Rust toolchain (if building from source): rustc 1.101.0-nightly (db8f076d2 2026-10-03), LLVM 22.1.8
  • Installed via: from source: cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static -p perry-ext-http, then scripts/gc_call_effects/sidecars.sh target/release

Diagnostic output

./leak small:

small 100000 VmRSS:    80484 kB
small 200000 VmRSS:   136644 kB
small 300000 VmRSS:   229212 kB
small 400000 VmRSS:   234688 kB
small 500000 VmRSS:   411640 kB
small 600000 VmRSS:   417272 kB
small 700000 VmRSS:   422364 kB
small 800000 VmRSS:   427504 kB
small 900000 VmRSS:   433112 kB
small 1000000 VmRSS:  783588 kB

thread '<unnamed>' panicked at crates/perry-stdlib/src/streams/idalloc.rs:130:9:
Web Streams handle id band exhausted: 1048576 ids live or quarantined (see PERRY_STREAM_ID_QUARANTINE)
Aborted (core dumped)

Max RSS 1.13 GB. Reproduced twice.

Controls, 1 100 000 calls each, both complete:

Server Response RSS at 1 000 000 / 1 100 000 calls Max RSS
keep-alive (above) 16 KiB (./leak big) 52 / 58 MB 67 MB
python3 -m http.server (HTTP/1.0, connection closed after each response), file with 16 bytes 16 bytes 51 / 46 MB 51 MB

Anything else

  • The retired-id quarantine holds at most 16 384 ids by default (PERRY_STREAM_ID_QUARANTINE), so reaching 1 048 576 means these ids stay live rather than retired.
  • The release 0.5.1520 shows the same growth (78 → 258 MB over 300 000 calls).
  • Impact for us: a server that reaches its database through fetch to a local API aborts after about a million calls.
Dominant language
Rust
Stars
4.9k
Forks
166
Avg merge
2h 54m
Merged PRs (30d)
922

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from PerryTS/perry

All issues in PerryTS/perry

Similar issues

More Rust issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.