Skip to content

Resource instances leak WASM memory — no cleanup on stream disconnect #1153

@hubyrod

Description

@hubyrod

Bug: Resource.instantiate() nodes persist in WASM after consumer disconnects

Version: @skipruntime/core@0.0.19

Problem

When a client disconnects from an SSE stream backed by a Resource, the reactive graph nodes created by Resource.instantiate() remain in WASM memory. There is no automatic cleanup, and the required manual cleanup method (serviceBroker.deleteUUID()) is not documented.

Without explicit cleanup, WASM arrayBuffers memory grows unbounded with each new SSE connection.

Reproduction

// Server-side: creating a resource instance per SSE client
app.get("/stream/:resourceName", async (req, reply) => {
  const uuid = await serviceBroker.getStreamUUID(
    req.params.resourceName,
    { userId: req.user.id }
  );

  // Client connects to the Skip streaming server
  reply.redirect(`http://localhost:8080/streams/${uuid}`);

  // When the client disconnects...
  // The reactive graph nodes for this UUID remain in WASM memory forever.
  // No automatic cleanup happens.
});

After many connect/disconnect cycles:

# Memory grows steadily:
# t=0:    arrayBuffers: 12 MB
# t=1h:   arrayBuffers: 85 MB
# t=4h:   arrayBuffers: 310 MB
# t=12h:  OOM kill

Current workaround

Manually calling serviceBroker.deleteUUID() when the stream consumer disconnects:

app.get("/stream/:resourceName", async (req, reply) => {
  const uuid = await serviceBroker.getStreamUUID(resourceName, params);

  // Proxy the SSE stream...
  const cleanup = () => {
    // Free the reactive graph nodes from WASM memory
    serviceBroker.deleteUUID(uuid).catch((err) => {
      console.warn(`Failed to delete stream ${uuid}: ${err.message}`);
    });
  };

  req.raw.on("close", cleanup);
  req.raw.on("error", cleanup);
});

We also built a memory trend tracker that samples process.memoryUsage().arrayBuffers every 60 seconds and alerts when growth exceeds 50 MB/hour — this is how we discovered the leak in the first place.

Expected behavior

One of:

  1. Automatic cleanup: When the SSE stream consumer disconnects (the HTTP connection closes), the runtime should detect this and release the associated resource instance and its reactive graph nodes from WASM memory.

  2. Documented manual cleanup: If automatic cleanup isn't feasible, deleteUUID() should be prominently documented as a required cleanup step, with examples showing the pattern for SSE stream lifecycle management.

Currently, deleteUUID() is not mentioned in any documentation or examples, making this a hidden memory leak that only surfaces under sustained production traffic.

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