Photo by Franck V. on Unsplash

Thread Stack Size: Memory Footprint vs. Deep Call Chains


Every thread you create comes with a stack, and that stack has a size determined before the thread starts running. This seemingly minor detail has major implications for how many threads your application can support and how much memory it actually uses.

The Default Stack Size Problem

Most operating systems assign each thread a default stack size between 1MB and 8MB. On Linux, the typical default is 8MB. On Windows, it’s 1MB. This might sound reasonable until you consider what happens when you create thousands of threads.

A server application that spins up 10,000 threads with 8MB stacks will reserve 80GB of virtual address space. The actual physical memory usage depends on how much of each stack gets touched, but the address space reservation alone creates real constraints. On 32-bit systems, this quickly exhausts the available address space. Even on 64-bit systems, the memory management overhead and page table entries add up.

The irony is that most threads never use more than a few kilobytes of their allocated stack. Web servers handling simple HTTP requests, worker threads processing queue items, or connection handlers in a database proxy typically have shallow call chains and small local variables. They’re running with massive stacks they’ll never need.

Why Stacks Are Sized at Thread Creation

Unlike heap memory, which grows dynamically, thread stacks are typically allocated as a single contiguous region. The operating system needs to reserve the entire address range up front, even if it uses lazy allocation techniques like demand paging to avoid committing physical pages until they’re accessed.

This pre-sizing exists because stack growth is difficult to handle at runtime. When a function call needs more stack space than is available, there’s no safe, generic way to pause execution, allocate more memory, and resume. Some systems implement growable stacks with guard pages that trigger expansion, but this adds complexity and performance overhead.

The JVM takes a different approach with its virtual threads implementation, using heap-allocated, resizable stacks. This enables millions of virtual threads because each stack can start small and grow only as needed. But native OS threads remain bound by the fixed-size model.

The Measurement Gap

The disconnect between allocated and used stack space stems from a measurement problem. Developers rarely profile stack usage because it’s not as visible as heap allocations. Profilers and memory tools focus on the heap, where dynamic allocation patterns matter more for typical performance issues.

When teams do measure stack usage, they often find that 95% of their threads use less than 100KB of stack space, even with several layers of function calls. Recursive algorithms and deeply nested parsers are the exception, not the rule.

Tuning Stack Size

Most runtime environments let you configure thread stack size. In Java, the -Xss flag controls it. In C with pthreads, the pthread_attr_setstacksize function sets it before thread creation. Go’s goroutines start with a 2KB stack that grows automatically, one reason Go can support hundreds of thousands of concurrent goroutines.

Reducing stack size from 8MB to 512KB can be safe for most application threads, cutting memory overhead by 93% and enabling 15x more threads in the same address space. The risk is stack overflow if you misjudge your application’s needs, but this typically manifests quickly in testing.

Some applications use different stack sizes for different thread pools. Threads handling network I/O might run with 256KB stacks, while threads executing user scripts get 2MB to handle deeper call chains safely.

The Virtual Memory Angle

Virtual memory makes stack allocation less painful than it could be. An 8MB stack reservation doesn’t immediately consume 8MB of RAM. The OS only commits physical pages as the stack grows into them. This lazy allocation means a thread that uses 50KB of its 8MB stack might only consume 50KB of physical memory.

But virtual memory isn’t free. Each stack needs page table entries, and the kernel tracks all these address ranges. TLB pressure increases, and address space fragmentation can prevent large contiguous allocations elsewhere. The memory management subsystem pays a cost for every reserved region, used or not.

Architecture Implications

The fixed stack size model influences architectural decisions. It’s one reason why thread-per-connection designs fell out of favor for high-concurrency servers. When each connection ties up megabytes of address space, you hit scaling limits well before exhausting CPU or I/O capacity.

Event-driven architectures, async/await models, and user-space threading systems all sidestep the stack size problem by avoiding OS threads or sharing stacks across many logical tasks. These designs have their own complexity costs, but they eliminate the memory overhead of thousands of mostly-empty stacks.

Understanding thread stack allocation helps explain why language runtime designers make certain tradeoffs and why replacing threads with lighter-weight concurrency primitives keeps appearing as a solution to scalability problems.