How Much Memory Do 1 Million Concurrent Tasks Consume in Different Languages?

This article benchmarks the peak memory usage of one, ten thousand, one hundred thousand, and one million concurrent tasks across Rust, Go, Java, C#, Node.js, Python, and Elixir, revealing surprising differences in runtime memory footprints and scalability.

Architect's Guide
Architect's Guide
Architect's Guide
How Much Memory Do 1 Million Concurrent Tasks Consume in Different Languages?

Introduction

The author observed that programs handling large numbers of network connections exhibited memory consumption differences of up to 20×, prompting the creation of a comprehensive benchmark to compare async and multithreaded runtimes across several popular languages.

Benchmark Design

Each program launches N concurrent tasks, each sleeping for 10 seconds, then exits after all tasks complete. The task count is supplied via a command‑line argument. All source code is available in the linked GitHub repository.

Rust

Three variants were written: a traditional thread version, a Tokio async version, and an async‑std version (similar to Tokio, omitted for brevity).

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();
}
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

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: classic threads and JDK 21 preview virtual threads.

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();
}
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#

List<Task> tasks = new List();
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(R) Xeon(R) E3-1505M v6 @ 3.00GHz

OS: Ubuntu 22.04 LTS

Rust 1.69, Go 1.18.1, Java OpenJDK 21‑ea, .NET 6.0.116, Node.js v12.22.9, Python 3.10.6, Elixir 1.12.2

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

Results

Minimum Memory (1 task)

Go and Rust (statically compiled) use the least memory; managed runtimes (Java, .NET, Node.js, Python) consume an order of magnitude more. .NET shows the highest baseline memory usage.

10 k Tasks

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

100 k Tasks

Thread‑based benchmarks could not be run on the author’s machine; attempting to start 100 k native threads failed. Go, Java, C#, and Node.js all outperform Rust in this range, while .NET shows little increase, possibly due to pre‑allocation.

1 Million Tasks

Elixir hits a system limit unless the Erlang VM is started with a higher process limit. C#’s memory usage rises but remains competitive, even slightly beating a Rust runtime. Go’s memory consumption grows dramatically, exceeding Rust by more than 12× and Java by over 2×. Tokio‑based Rust remains the most memory‑efficient async runtime at this scale.

Final Thoughts

Massive numbers of concurrent tasks can consume substantial memory even when the tasks perform no work. Runtime implementations differ: some have low initial overhead but scale poorly, while others have higher baseline memory usage but handle large loads gracefully. The benchmark focuses solely on memory; other factors such as task‑startup latency and communication throughput are also important. The author plans future benchmarks to explore those dimensions.

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.

JavaPythonConcurrencyRustGoNode.jsCbenchmarkAsyncElixirmemory consumption
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.