Title: tls: createSecureContext() drops options.singleUse, so per-connection contexts live until GC · Issue #66002 · nodejs/node · GitHub
Open Graph Title: tls: createSecureContext() drops options.singleUse, so per-connection contexts live until GC · Issue #66002 · nodejs/node
X Title: tls: createSecureContext() drops options.singleUse, so per-connection contexts live until GC · Issue #66002 · nodejs/node
Description: Version 22.23.2 Platform Amazon Linux 2023, x86_64 and aarch64 Subsystem No response What steps will reproduce the bug? Version v22.23.2 (verified at runtime); same code present in v24.18.0, v26.8.2, v26.x and main Platform Linux (Amazon...
Open Graph Description: Version 22.23.2 Platform Amazon Linux 2023, x86_64 and aarch64 Subsystem No response What steps will reproduce the bug? Version v22.23.2 (verified at runtime); same code present in v24.18.0, v26.8....
X Description: Version 22.23.2 Platform Amazon Linux 2023, x86_64 and aarch64 Subsystem No response What steps will reproduce the bug? Version v22.23.2 (verified at runtime); same code present in v24.18.0, v26.8....
Opengraph URL: https://github.com/nodejs/node/issues/66002
X: @github
Domain: github.com
{"@context":"https://schema.org","@type":"DiscussionForumPosting","headline":"tls: createSecureContext() drops options.singleUse, so per-connection contexts live until GC","articleBody":"### Version\n\n22.23.2\n\n### Platform\n\n```text\nAmazon Linux 2023, x86_64 and aarch64\n```\n\n### Subsystem\n\n_No response_\n\n### What steps will reproduce the bug?\n\n### Version\nv22.23.2 (verified at runtime); same code present in v24.18.0, v26.8.2, v26.x and main\n\n### Platform\nLinux (Amazon Linux 2023, x86_64 and aarch64), OpenSSL 3.5.7, glibc malloc — not platform specific\n\n### Subsystem\ntls\n\n### What steps will reproduce the bug?\n\n```js\nconst tls = require('node:tls');\n\nconst socket = tls.connect({ host: 'example.com', port: 443 }, () => {\n console.log(socket.ssl._secureContext.singleUse); // undefined — expected true\n socket.end();\n});\n```\n\n`tls.connect()` sets `options.singleUse = true` (lib/internal/tls/wrap.js), but the context\nit builds never carries the flag.\n\n### How often does it reproduce?\nAlways.\n\n### What is the expected behavior?\n\n`TLSSocket.prototype._destroySSL()` and `TLSWrap.prototype.close()` both check\n`ssl._secureContext.singleUse` and call `context.close()`, which frees the underlying\n`SSL_CTX` when the socket closes. Up to v15.14.0 `createSecureContext()` copied the flag\nonto the context it returned (lib/_tls_common.js):\n\n```js\n if (options.singleUse) {\n c.singleUse = true;\n c.context.setFreeListLength(0);\n }\n```\n\n### What do you see instead?\n\nSince v16.0.0, when context configuration moved into `configSecureContext()`, that copy was\ndropped. `singleUse` now appears only in `lib/internal/tls/wrap.js` — nothing assigns it to a\ncontext — so `ssl._secureContext.singleUse` is always `undefined`, `context.close()` is never\ncalled, and each connection's `SSL_CTX` lives until V8 collects the JS `SecureContext` wrapper.\n\nTwo things make that costly:\n\n* `SecureContext` reports `kExternalSize = 1024` bytes to V8 (src/crypto/crypto_context.h),\n while an OpenSSL 3 `SSL_CTX` is roughly 16 KB — and far larger when `ca` is supplied, since\n the context then holds its own parsed copy of the root store. V8 therefore sees almost no\n reason to collect. (`TLSWrap` accounts ~50 KB but subtracts it in `Destroy()` at close, so\n nothing remains on the books.)\n* In a process with a low allocation rate, major GCs can be more than an hour apart, so the\n dead contexts accumulate for that whole period.\n\nMeasured with `malloc_stats()` before and after a forced full GC, after ~200 sequential\nclient connections (Node v22.23.2):\n\n| | in use after churn | after full GC | held per closed socket |\n|---|---|---|---|\n| as shipped (context per connection) | 7.24 MB | 4.09 MB | ~16 KB |\n| `createSecureContext` patched to copy `singleUse` | 3.99 MB | 3.99 MB | ~0 |\n| one shared `secureContext` passed to `tls.connect` | 3.99 MB | 3.98 MB | ~0 |\n\nProduction impact for us: a MongoDB connection pool recycling idle connections\n(`minPoolSize: 5`, `maxIdleTimeMS: 60000`, four worker threads) grew RSS ~4.5 MB per 10\nminutes with trust supplied through `NODE_EXTRA_CA_CERTS`, and ~113 MB per 10 minutes when\nthe CA bundle was passed as `tlsCAFile` — each connection's context then parses its own copy\nof the 96-certificate bundle. Passing one shared `secureContext` stops the growth entirely.\n\n### Additional note: the keepAlive condition\n\n`tls.connect()` only sets `singleUse` when `!options.keepAlive`. A client that enables TCP\nkeepalive (the MongoDB driver sets `keepAlive: true` unconditionally) therefore never gets the\nearly close even if the propagation is restored. Since the context is built per connection\neither way, and keepAlive concerns the TCP socket rather than the TLS context, should the\ncontext be closed on socket close regardless of that flag?\n\n### Possible fixes\n\n1. Restore the copy in `createSecureContext()` — one line.\n2. Report a realistic external size for `SecureContext` so V8 accounts for it.\n3. Reconsider the `keepAlive` condition, per the note above.\n\n### How often does it reproduce? Is there a required condition?\n\nevery time\n\n### What is the expected behavior? Why is that the expected behavior?\n\nto release memory\n\n### What do you see instead?\n\nmemory keep increasing\n\n### Additional information\n\n_No response_","author":{"url":"https://github.com/naturalfreak","@type":"Person","name":"naturalfreak"},"datePublished":"2026-09-12T15:58:24Z","url":"https://github.com/nodejs/node/issues/66002"}
| react-profiling | 0 |
| react-import-map | react,react-dom,react-dom/client,react-dom/profiling,react-is,react-reconciler,react/compiler-runtime,react/jsx-dev-runtime,react/jsx-runtime,scheduler@d5750093facf |
| route-pattern | /_view_fragments/issues/show/:user_id/:repository/:id/issue_layout(.:format) |
| route-controller | voltron_issues_fragments |
| route-action | issue_layout |
| fetch-nonce | v2:8f8c941c-da34-f18e-d143-06995a037a7f |
| current-catalog-service-hash | 81bb79d38c15960b92d99bca9288a9108c7a47b18f2423d0f6438c5b7bcd2114 |
| request-id | A5A2:2C9BD4:A89C1:E88F7:6AED3348 |
| html-safe-nonce | a37e066f858d4ac7c4d8a838e540b90d8dc9525e6151d6792ae61cf5267b7947 |
| visitor-payload | eyJyZWZlcnJlciI6IiIsInJlcXVlc3RfaWQiOiJBNUEyOjJDOUJENDpBODlDMTpFODhGNzo2QUVEMzM0OCIsInZpc2l0b3JfaWQiOiI4NjgwOTc1NzUwMDA2NjQ5NjcyIiwicmVnaW9uX2VkZ2UiOiJpYWQiLCJyZWdpb25fcmVuZGVyIjoiaWFkIn0= |
| visitor-hmac | 59ffa802c26c8a36d7387468eb08b2c0d57c3fb7965e552a46307b51e4858c4a |
| hovercard-subject-tag | issue:5434388397 |
| github-keyboard-shortcuts | repository,issues,copilot |
| google-site-verification | Apib7-x98H0j5cPqHWwSMm6dNU4GmODRoqxLiDzdx9I |
| octolytics-url | https://collector.github.com/github/collect |
| analytics-location | / |
| fb:app_id | 1401488693436528 |
| apple-itunes-app | app-id=1477376905, app-argument=https://github.com/_view_fragments/issues/show/nodejs/node/66002/issue_layout |
| twitter:image | https://opengraph.githubassets.com/76b5759e0d37e382194442d4358d37bbe4f4f5ce2a6183551d227c1b0af5ddfb/nodejs/node/issues/66002 |
| twitter:card | summary_large_image |
| og:image | https://opengraph.githubassets.com/76b5759e0d37e382194442d4358d37bbe4f4f5ce2a6183551d227c1b0af5ddfb/nodejs/node/issues/66002 |
| og:image:alt | Version 22.23.2 Platform Amazon Linux 2023, x86_64 and aarch64 Subsystem No response What steps will reproduce the bug? Version v22.23.2 (verified at runtime); same code present in v24.18.0, v26.8.... |
| og:image:width | 1200 |
| og:image:height | 600 |
| og:site_name | GitHub |
| og:type | object |
| og:author:username | naturalfreak |
| hostname | github.com |
| expected-hostname | github.com |
| None | e51d0b7b5ab34b1688c05b54c3ed17b8658b2fa9bca973a9c740683ccb12cf4a |
| turbo-cache-control | no-preview |
| go-import | github.com/nodejs/node git https://github.com/nodejs/node.git |
| octolytics-dimension-user_id | 9950313 |
| octolytics-dimension-user_login | nodejs |
| octolytics-dimension-repository_id | 27193779 |
| octolytics-dimension-repository_nwo | nodejs/node |
| octolytics-dimension-repository_public | true |
| octolytics-dimension-repository_is_fork | false |
| octolytics-dimension-repository_network_root_id | 27193779 |
| octolytics-dimension-repository_network_root_nwo | nodejs/node |
| turbo-body-classes | logged-out env-production page-responsive |
| disable-turbo | false |
| browser-stats-url | https://api.github.com/_private/browser/stats |
| browser-errors-url | https://api.github.com/_private/browser/errors |
| release | 59ebb7b901cc154ecc179a44f266fcf69f343b30 |
| ui-target | full |
| theme-color | #1e2327 |
| color-scheme | light dark |
| react-app-name | issues |
Links:
Viewport: width=device-width