Skip to content

[BUG] Memory leak in Rust Durable Object eviction #722

@lukevalenta

Description

@lukevalenta

Is there an existing issue for this?

  • I have searched the existing issues

What version of workers-rs are you using?

0.5.0

What version of wrangler are you using?

4.10.0

Describe the bug

In Rust Workers, memory allocated for a Durable Object is not freed upon eviction. I'm able to reproduce locally with miniflare, and seem to have hit this same issue in a production worker (cloudflare/azul#11).

Steps To Reproduce

With the following files:

cat Cargo.toml
[package]
name = "memory-leak"
version = "0.1.0"
edition = "2021"

[package.metadata.release]
release = false

# https://github.com/rustwasm/wasm-pack/issues/1247
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

[lib]
crate-type = ["cdylib"]

[dependencies]
worker = { version = "0.5.0" }

cat wrangler.jsonc
{
	"name": "memory-leak",
    "main": "build/worker/shim.mjs",
	"build": {
		"command": "cargo install -q worker-build && worker-build --release"
	},
	"compatibility_date": "2025-04-08",
	"migrations": [
		{
			"new_sqlite_classes": [
				"MyDurableObject"
			],
			"tag": "v1"
		}
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "MyDurableObject",
				"name": "MY_DURABLE_OBJECT"
			}
		]
	},
	"observability": {
		"enabled": true
	}
}

cat src/lib.rs
use wasm_bindgen::prelude::*;
#[allow(clippy::wildcard_imports)]
use worker::*;

#[event(fetch, respond_with_errors)]
async fn main(_req: Request, env: Env, _ctx: Context) -> Result<Response> {
    let ns = env.durable_object("MY_DURABLE_OBJECT")?;
    let id = ns.id_from_name("foo")?;
    let stub = id.get_stub()?;
    stub.fetch_with_str("http://example.com").await
}

#[durable_object]
struct MyDurableObject {
    count: u64,
    _buffer: Vec<u8>,
}

#[durable_object]
impl DurableObject for MyDurableObject {
    fn new(state: State, _env: Env) -> Self {
        Self {
            count: 0,
            _buffer: Vec::with_capacity(100_000_000),
        }
    }
    #[allow(clippy::unused_async)]
    async fn fetch(&mut self, mut _req: Request) -> Result<Response> {
        self.count += 1;
        Response::ok(format!("hello {}", self.count))
    }
}

Then, run npx wrangler dev and hit the d key to open devtools. Now, take a heap memory snapshot, and then run the following in another terminal window to repeatedly re-initialize the DO (after it is evicted after 10s of inactivity):

while true; do curl http://localhost:8787; sleep 10; done

Take memory snapshots after every request and we can see it's continuously increasing by 100MB:
Image

Now, with a JavaScript worker, we can see that there is no leak:

cat package.json
{
	"name": "memory-leak-js",
	"version": "0.0.0",
	"private": true,
	"scripts": {
		"deploy": "wrangler deploy",
		"dev": "wrangler dev",
		"start": "wrangler dev"
	},
	"devDependencies": {
		"wrangler": "^4.9.1"
	}
}
cat wrangler.jsonc
{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "memory-leak-js",
	"main": "src/index.js",
	"compatibility_date": "2025-04-08",
	"migrations": [
		{
			"new_sqlite_classes": [
				"MyDurableObject"
			],
			"tag": "v1"
		}
	],
	"durable_objects": {
		"bindings": [
			{
				"class_name": "MyDurableObject",
				"name": "MY_DURABLE_OBJECT"
			}
		]
	},
	"observability": {
		"enabled": true
	}
}
cat src/index.js
import { DurableObject } from "cloudflare:workers";

export class MyDurableObject extends DurableObject {
	constructor(ctx, env) {
		super(ctx, env);
		this.count = 0;
		this.buffer = new Uint8Array(100_000_000)
	}

	async fetch() {
		this.count += 1
		return new Response('hello ' + this.count);
	}
}

export default {
	async fetch(request, env, ctx) {
    console.log('fetch');
		const id = env.MY_DURABLE_OBJECT.idFromName("foo");
		const stub = env.MY_DURABLE_OBJECT.get(id);
		return await stub.fetch("http://example.com");
	},
};

In devtools, I see this memory usage. The final snapshot is after I allowed the DO to be fully evicted (killing the script from earlier):

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions