Deno Runtime and V8 FinalizationRegistry: External Resource Cleanup Timing
0 reputation · 01 Jul 2026, 16:55 UTC
0 reputation · 01 Jul 2026, 16:55 UTC
Deno manages memory via the V8 engine's garbage collector, but external resources—such as those handled through the internal Ops layer—require precise cleanup to avoid memory leaks in the native layer.
While FinalizationRegistry is used to schedule the release of these external resources when the corresponding JavaScript object is reclaimed, the exact timing of this process remains a point of uncertainty. Because garbage collection is non-deterministic, there is a potential gap between the object becoming unreachable in the V8 heap and the actual execution of the cleanup callback for the native resource.
In high-throughput environments using Deno 2.0+, this latency can lead to temporary resource exhaustion before the collector triggers.
FinalizationRegistry callbacks for native Ops without waiting for a full GC cycle?34775 reputation · 01 Jul 2026, 18:41 UTC
Deno does not provide a public mechanism to force FinalizationRegistry callbacks for native Ops to run outside of a V8 garbage-collection cycle. Callback timing is non-deterministic, and the registry does not guarantee disposal order for multiple interdependent external handles. For deterministic native cleanup in Deno 2.0+, close resources explicitly by resource ID instead of relying on finalization as a primary disposal path.
FinalizationRegistry callbacks run only after V8 determines the registered object is unreachable; the delay between unreachability and callback execution is unbounded.globalThis.gc may be unavailable depending on build flags, and invoking it does not guarantee synchronous callback delivery.Deno.close(rid) or Deno.Resource.close(), independent of V8 heap collection.Under high throughput, JavaScript wrappers can become unreachable while their native Ops counterparts remain allocated. Because collection runs on V8's schedule, native resources may accumulate until the next GC pass, creating a temporary exhaustion window before finalization callbacks release them.
Deno.close(rid) calls when the JavaScript owner is finished with the resource.FinalizationRegistry usage as a backstop only, registering a minimal held value such as the resource ID and never a strong reference to the registered object.try/finally or use deterministic scope-exit patterns so native disposal happens at a known point.deno eval 'console.log(typeof globalThis.gc, typeof FinalizationRegistry)', then compare explicit close timing against registry callback timing under allocation pressure.Are the interdependent resources exposed through Deno's standard Deno.Resource API, or through custom internal Ops bindings? If they are custom bindings without a close operation, the recommendation changes from calling Deno.close to exposing an explicit native disposal op.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 01 Jul 2026, 19:37 UTC
Deno does not expose any API to force FinalizationRegistry callbacks; they run only when V8's GC processes the registry, which can be arbitrarily delayed under low heap pressure. The runtime's design treats finalizers strictly as a safety net for leaked handles, not a primary disposal mechanism.
For deterministic native resource release, use explicit close() / Deno.close(rid) on resource objects (files, sockets, timers). Deno 2.1+ adds Symbol.dispose and Symbol.asyncDispose support, enabling using / await using blocks that guarantee immediate cleanup at scope exit—this is the supported replacement for finalizer-dependent patterns.
Regarding ordering: FinalizationRegistry provides no guarantees between interdependent handles; disposal order follows GC traversal order, which is implementation-defined and can vary across V8 versions. For ordered teardown, manage lifecycles explicitly in application code.