How Much Memory Do 1 Million Concurrent Tasks Use Across Different Languages?

This benchmark measures the peak memory consumption of one, ten‑thousand, one‑hundred‑thousand and one‑million concurrent tasks in Rust, Go, Java, C#, Node.js, Python and Elixir, revealing surprising differences such as Go using far more memory than Rust and .NET showing unusually low usage.

Architect's Guide
Architect's Guide
Architect's Guide
How Much Memory Do 1 Million Concurrent Tasks Use Across Different Languages?

Introduction

The author created a set of programs that launch N concurrent tasks, each sleeping for ten seconds, then exit. The number of tasks is supplied via a command‑line argument. All implementations are available in a public GitHub repository.

Benchmark Implementations

Rust

Three variants were written: a traditional thread version and two async versions using tokio and async‑std. The thread version creates a Vec of handles, spawns a thread that sleeps for ten seconds, pushes the handle, and joins all handles after the loop.

let mut handles = Vec::new();
for _ in 0..num_threads {
    let handle = thread::spawn(|| {
        thread::sleep(Duration::from_secs(10));
    });
    handles.push(handle);
}
for handle in handles {
    handle.join().unwrap();
}

The tokio async version creates a Vec of tasks, spawns each with task::spawn, sleeps asynchronously, and then awaits each task.

let mut tasks = Vec::new();
for _ in 0..num_tasks {
    tasks.push(task::spawn(async {
        time::sleep(Duration::from_secs(10)).await;
    }));
}
for task in tasks {
    task.await.unwrap();
}

Go

Goroutines are launched inside a for loop, each sleeping for ten seconds, and a sync.WaitGroup is used to wait for completion.

var wg sync.WaitGroup
for i := 0; i < numRoutines; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        time.Sleep(10 * time.Second)
    }()
}
wg.Wait()

Java

Two versions were written: a classic thread version and a virtual‑thread version (preview in JDK 21). Both create a list of Thread objects, start them, and join after the loop.

List<Thread> threads = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {
    Thread thread = new Thread(() -> {
        try { Thread.sleep(Duration.ofSeconds(10)); } catch (InterruptedException e) {}
    });
    thread.start();
    threads.add(thread);
}
for (Thread thread : threads) {
    thread.join();
}

The virtual‑thread version replaces new Thread with Thread.startVirtualThread, keeping the rest identical.

List<Thread> threads = new ArrayList<>();
for (int i = 0; i < numTasks; i++) {
    Thread thread = Thread.startVirtualThread(() -> {
        try { Thread.sleep(Duration.ofSeconds(10)); } catch (InterruptedException e) {}
    });
    threads.add(thread);
}
for (Thread thread : threads) {
    thread.join();
}

C#

Async/await is used with Task.Run to create tasks that delay for ten seconds, then Task.WhenAll waits for all.

List<Task> tasks = new List<Task>();
for (int i = 0; i < numTasks; i++) {
    Task task = Task.Run(async () => {
        await Task.Delay(TimeSpan.FromSeconds(10));
    });
    tasks.Add(task);
}
await Task.WhenAll(tasks);

Node.js

const delay = util.promisify(setTimeout);
const tasks = [];
for (let i = 0; i < numTasks; i++) {
    tasks.push(delay(10000));
}
await Promise.all(tasks);

Python

async def perform_task():
    await asyncio.sleep(10)

tasks = []
for _ in range(num_tasks):
    task = asyncio.create_task(perform_task())
    tasks.append(task)
await asyncio.gather(*tasks)

Elixir

tasks = for _ <- 1..num_tasks do
    Task.async(fn ->
        :timer.sleep(10000)
    end)
end
Task.await_many(tasks, :infinity)

Test Environment

CPU: Intel Xeon E3‑1505M v6 @ 3.00 GHz

OS: Ubuntu 22.04 LTS

Rust 1.69, Go 1.18.1, Java OpenJDK 21‑ea, .NET 6.0.116, Node v12.22.9, Python 3.10.6, Elixir 1.12.2 (Erlang/OTP 24)

All programs were compiled/run in release mode with default settings.

Results

Minimum Memory (1 task)

Two groups emerge: native‑compiled Rust and Go binaries use very little memory, while managed runtimes (Java, .NET, Node, Python, Elixir) consume an order of magnitude more. Surprisingly, .NET shows the highest usage among the managed group.

10 000 Tasks

Java threads consume ~250 MiB, confirming the expectation that threads are memory‑hungry. Rust native threads remain lightweight, staying below many runtimes' idle memory. Go’s goroutine memory is about 50 % higher than Rust’s, contrary to the belief that goroutines are extremely cheap.

100 000 Tasks

Thread‑based benchmarks cannot be run at this scale on the author’s machine. Go loses its earlier advantage and consumes over six times the memory of the best Rust program, and is also outperformed by Python.

1 000 000 Tasks

Elixir crashes with a system‑limit error unless the Erlang VM is started with +P 1000000. C#’s memory usage rises but remains competitive, even slightly beating one Rust runtime. Go’s memory consumption grows dramatically, exceeding Rust by more than twelve times and Java by over two times. The tokio runtime still shows the lowest memory footprint among async solutions.

Conclusion

Running a large number of concurrent tasks can consume substantial memory even when the tasks perform no work. Languages with low‑overhead runtimes (Rust native threads, Rust async) stay efficient at scale, while others incur high per‑task costs that become prohibitive beyond a few hundred thousand tasks. The benchmark focuses solely on memory; latency and throughput are left for future work.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaConcurrencyRustGoC++memory usageasync runtimes
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.