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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
