You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
After await registry.shutdown() resolves, a native (NAPI) RivetKit process never exits on its own. The event loop is held open by referenced uv async handles, and the only way out is an external signal or process.exit().
It reproduces with stock RivetKit 2.3.17 and no application code: an empty registry, a registry with a plain actor, and a registry with a db() actor. It happens under both Bun and Node.
engine/sdks/rust/envoy-client/src/envoy.rs stores the serve-mode envoy handle in the process-wide static GLOBAL_ENVOY. It is only replaced by a later start and is never cleared on shutdown.
rivetkit-coreregistry/mod.rsserve_with_config_and_handle_observer passes not_global: false. The cached handle holds RegistryCallbacks → dispatcher → factories → CallbackBindings. Those contain the NAPI ThreadsafeFunctions created in rivetkit-napi/src/actor_factory.rs (create_tsfn), which are referenced by default. These appear to be the live uv async handles.
There is no public API to clear that slot or unref the TSFNs. serverless.rs already uses not_global: true, and its comments acknowledge the retained-handle behavior.
For the alarm error: the TS onSleep/onDestroy path calls closeNativeSqlDatabase before Rust finish_shutdown_cleanup_with_ctx calls sync_alarm_logged.
Possible fix
After a completed shutdown, clear GLOBAL_ENVOY, or drop/unref the callback TSFNs as part of shutdown. Separately, sync alarms before the SQL coordinator closes, or skip the sync once it is closed. I'm happy to open a PR if a direction is preferred.
Description
After
await registry.shutdown()resolves, a native (NAPI) RivetKit process never exits on its own. The event loop is held open by referenced uv async handles, and the only way out is an external signal orprocess.exit().It reproduces with stock RivetKit 2.3.17 and no application code: an empty registry, a registry with a plain actor, and a registry with a
db()actor. It happens under both Bun and Node.Environment
2.3.17,runtime: "native", a serverful remote Engine2.3.17(local, disposable)1.4.2and Node26.8.1, macOS arm642.3.18-rc.2andmain(78336a1): the relevant code is unchangedReproduction
Observed
shutdown resolvedprints;exitnever does. In 5 out of 5 runs (bun/node × empty/no-db/sql), none exited within 20 s after shutdown.asynchandles after shutdown. A forced GC (--expose-gc) does not release them.db()actor, shutdown also logstransaction_closedforSELECT MIN(trigger_at) FROM _rivet_schedule_eventsfollowed byfailed to sync scheduled actor alarm. This looks like registry.shutdown() queries scheduled alarms after SQLite coordinator closes #5555 still reproducing on 2.3.17.Expected
Once
registry.shutdown()resolves, the process exits naturally, with no leftover referenced handles.Source analysis (at the 2.3.17 tag, fcf9535)
engine/sdks/rust/envoy-client/src/envoy.rsstores the serve-mode envoy handle in the process-wide staticGLOBAL_ENVOY. It is only replaced by a later start and is never cleared on shutdown.rivetkit-coreregistry/mod.rsserve_with_config_and_handle_observerpassesnot_global: false. The cached handle holdsRegistryCallbacks→ dispatcher → factories →CallbackBindings. Those contain the NAPIThreadsafeFunctions created inrivetkit-napi/src/actor_factory.rs(create_tsfn), which are referenced by default. These appear to be the live uv async handles.serverless.rsalready usesnot_global: true, and its comments acknowledge the retained-handle behavior.onSleep/onDestroypath callscloseNativeSqlDatabasebefore Rustfinish_shutdown_cleanup_with_ctxcallssync_alarm_logged.Possible fix
After a completed shutdown, clear
GLOBAL_ENVOY, or drop/unref the callback TSFNs as part of shutdown. Separately, sync alarms before the SQL coordinator closes, or skip the sync once it is closed. I'm happy to open a PR if a direction is preferred.