# How workerd Handles Memory Management and tcmalloc: Build Configuration and Runtime Optimization

> Discover how workerd optimizes memory management with tcmalloc by enabling a Bazel build flag. Reduce allocation latency and fragmentation on Linux.

- Repository: [Cloudflare/workerd](https://github.com/cloudflare/workerd)
- Tags: internals
- Published: 2026-03-18

---

**workerd replaces the standard C allocator with tcmalloc (Google's high-performance malloc) on Linux via a Bazel build flag to reduce allocation latency and fragmentation under heavy multi-threaded workloads.**

The Cloudflare workerd runtime manages memory-intensive operations across thousands of concurrent green threads. According to the cloudflare/workerd source code, the project implements a configurable memory allocation strategy that swaps the default system malloc for tcmalloc on Linux platforms, optimizing for the specific allocation patterns of edge computing workloads.

## Build-Time Allocator Selection in workerd

The memory management backend is determined at compile time through Bazel build configuration.

### The `use_tcmalloc` Bazel Flag

In `src/workerd/server/BUILD.bazel`, the project defines a boolean flag that controls allocator selection:

```python
bool_flag(
    name = "use_tcmalloc",
    build_setting_default = True,
)

```

This flag defaults to `True` for Linux builds, automatically enabling tcmalloc integration while allowing developers to opt out for specific toolchains.

### Target Selection and Linking

The build system uses a `select` statement to choose between tcmalloc and the standard library. The `wd_cc_library` target named `malloc` encapsulates this logic:

```python
wd_cc_library(
    name = "malloc",
    deps = select({
        ":really_use_tcmalloc": ["@tcmalloc//tcmalloc"],
        "//conditions:default": ["@bazel_tools//tools/cpp:malloc"],
    }),
)

```

The `workerd` binary links against this target via `malloc = ":malloc"`, ensuring all `malloc` and `free` calls route through the selected implementation. When the `:really_use_tcmalloc` condition is satisfied, the binary links `@tcmalloc//tcmalloc`; otherwise, it uses the system allocator.

### Disabling tcmalloc for Sanitizer Builds

To build with AddressSanitizer or other memory debugging tools, disable tcmalloc explicitly:

```bash
bazel build //src/workerd/server:workerd \
  --//src/workerd/server:use_tcmalloc=False

```

This configuration passes the standard system malloc to the linker instead of the tcmalloc dependency, preventing conflicts with sanitizers that intercept allocation functions.

## Why tcmalloc Matters for workerd Performance

workerd spawns numerous green threads (KJ coroutines) that allocate buffers frequently. The standard allocator struggles with lock contention under this pattern.

### Mitigating Allocation Amplification

The streams implementation in `src/workerd/api/streams/internal.c++` uses large default read buffers of **128 KB** (`131072` bytes) to minimize allocation frequency. According to source comments, this prevents "allocation amplification" where many tiny buffers contend for tcmalloc's internal locks, potentially exceeding watchdog timeouts when thousands of green threads compete for memory:

```cpp
static constexpr uint64_t DEFAULT_BUFFER_CHUNK = 131072; // 128KB
// With many green threads contending on tcmalloc, the cumulative
// allocation overhead can exceed watchdog timeouts. Using 128KB
// default reads ensures tee chunks are consumed whole, eliminating
// the amplification entirely.

```

By allocating in large chunks, workerd reduces lock contention inside tcmalloc and improves throughput for I/O-bound operations.

## Deterministic Benchmarking with tcmalloc

tcmalloc's probabilistic sampling and background tasks can introduce non-determinism in performance measurements.

### Disabling Sampling for Benchmarks

The header [`src/workerd/tests/bench-tools.h`](https://github.com/cloudflare/workerd/blob/main/src/workerd/tests/bench-tools.h) defines `TcmallocBenchmarkConfig`, a structure that disables tcmalloc's runtime variability when `WD_USE_TCMALLOC` is defined:

```cpp
#ifdef WD_USE_TCMALLOC
#include "tcmalloc/malloc_extension.h"

struct TcmallocBenchmarkConfig {
  TcmallocBenchmarkConfig() {
    tcmalloc::MallocExtension::SetProfileSamplingInterval(
        std::numeric_limits<int64_t>::max());
    tcmalloc::MallocExtension::SetGuardedSamplingInterval(-1);
    tcmalloc::MallocExtension::SetBackgroundProcessActionsEnabled(false);
  }
};
inline TcmallocBenchmarkConfig tcmallocBenchmarkConfig;
#endif

```

This code disables heap profiling sampling, GWP-ASan guarded sampling, and background process actions to ensure repeatable benchmark timings across runs. The class is instantiated as an inline variable to ensure configuration runs before main execution.

## Summary

- **workerd** links against tcmalloc by default on Linux through the `use_tcmalloc` Bazel flag defined in `src/workerd/server/BUILD.bazel`.
- The allocator selection happens at build time via a `select` statement that chooses between `@tcmalloc//tcmalloc` and the system malloc.
- To reduce tcmalloc lock contention, workerd allocates stream buffers in 128 KB chunks as implemented in `src/workerd/api/streams/internal.c++`.
- Benchmarks disable tcmalloc sampling via `TcmallocBenchmarkConfig` in [`src/workerd/tests/bench-tools.h`](https://github.com/cloudflare/workerd/blob/main/src/workerd/tests/bench-tools.h) to ensure deterministic results.
- Sanitizer builds require explicitly disabling tcmalloc with `--//src/workerd/server:use_tcmalloc=False`.

## Frequently Asked Questions

### Does workerd use tcmalloc on all platforms?

No, tcmalloc is selected only on Linux builds where the `use_tcmalloc` flag defaults to true. On other platforms, workerd falls back to the standard system allocator provided by Bazel's `@bazel_tools//tools/cpp:malloc` target.

### How do I build workerd without tcmalloc?

Pass the Bazel command line flag `--//src/workerd/server:use_tcmalloc=False` when building the `//src/workerd/server:workerd` target. This configuration is required when running AddressSanitizer or other memory debugging tools that conflict with tcmalloc.

### Why does workerd use 128KB buffers for streams?

The 128KB default buffer size in `src/workerd/api/streams/internal.c++` minimizes the number of individual allocations made to tcmalloc. With thousands of green threads contending for the allocator's internal locks, many small allocations would cause "allocation amplification" and potential watchdog timeouts; large chunks eliminate this overhead by ensuring tee chunks are consumed whole.

### How does workerd ensure deterministic benchmark results?

The runtime disables tcmalloc's probabilistic heap profiling, GWP-ASan guarded sampling, and background release actions through the `TcmallocBenchmarkConfig` class defined in [`src/workerd/tests/bench-tools.h`](https://github.com/cloudflare/workerd/blob/main/src/workerd/tests/bench-tools.h). This initialization runs automatically when `WD_USE_TCMALLOC` is defined, ensuring consistent memory allocation behavior across benchmark iterations.