Skip to content

(Issue)- unbounded memory growth during actors cycling. #5607

Description

@itsme-boii

Summary:-
ActorContextShared::reset_runtime_state() in rivetkit-napi uses std::mem::forget to avoid a panic when dropping a napi::Ref on a Tokio worker thread (where no Node.js Env is available). This leaks one JS object reference slot per actor wake cycle. Over time in long-running processes with many actors cycling through sleep/wake, this is unbounded memory growth.

Problem:-
When an actor wakes from sleep, reset_runtime_state() is called to clear the previous JS state object. The napi::Ref type requires calling .unref(env) with a valid Node.js Env handle to properly release the reference. But reset_runtime_state() runs on a Tokio worker thread where no Env exists.

The current code uses std::mem::forget(old) to suppress the destructor. This avoids a debug_assert panic in napi-rs, but permanently leaks the underlying V8 reference slot.

The same pattern appears in the Drop impl for ActorContextShared.

File: rivetkit-typescript/packages/rivetkit-napi/src/actor_context.rs

if let Some(old) = self.runtime_state.lock().take() {
    std::mem::forget(old);
}

Impact:-

  • Rivet Actors are designed to sleep and wake frequently. An actor that wakes 100 times leaks 100 JS reference slots.
  • The leak is per-actor, per-wake. A deployment with thousands of actors cycling through sleep/wake accumulates leaked references without bound.
  • The only relief is process restart.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions