In 2025, the C standards committee finally added tail-call optimization to the language. Not as a compiler extension. Not as a vague suggestion buried in an appendix. As an actual language feature, standardized across compilers.
If you write Haskell, Scheme, or Erlang, you're probably confused. Those languages have had proper tail-call optimization since the 1970s and 1980s. Scheme's specification literally requires it. So why did C, arguably the most influential programming language in history, take over 50 years?
The answer is messier than you'd expect. It involves compiler engineers arguing at conferences, backwards compatibility nightmares, and a genuine philosophical disagreement about what C is supposed to be.
What Tail-Call Optimization Actually Means
A tail call happens when a function's last action is calling another function (or itself). Here's a simple example:
int factorial_tail(int n, int acc) {
if (n <= 1) return acc;
return factorial_tail(n - 1, n * acc); // tail call
}That return factorial_tail(...) is the last thing the function does. There's no multiplication waiting to happen after the call returns. The function just passes the result straight through.
In theory, the compiler can notice this and replace the recursive call with a jump. Instead of creating a new stack frame each time, it reuses the current one. The recursion becomes a loop under the hood. No stack overflow. No wasted memory. Same performance as a while loop.
Without tail-call optimization, that factorial function blows up your stack if you pass in a large number. With it, factorial_tail(1000000, 1) runs fine.
Simple idea. Obvious win. So what went wrong?
Functional Languages Got There First for a Reason
Languages like Scheme and Haskell were designed around recursion. In pure functional programming, you don't have mutable loop variables. You recurse. If recursion is your primary control flow mechanism, tail-call optimization isn't a nice-to-have, it's the difference between your programs working and your programs crashing.
Scheme made tail-call optimization mandatory in its original 1975 specification. Not optional. Not "quality of implementation." The language does not work without it.
C has a different story. C has for loops. C has while loops. C has goto. Recursion in C is something you reach for when a loop doesn't fit the problem cleanly, not your default way of repeating things. So from the committee's perspective, optimizing tail calls was a quality-of-life improvement, not a correctness requirement.
That framing mattered more than anyone expected.
The Technical Barriers Were Real
Here's something people forget when they complain about the committee being slow: C's execution model fights tail-call optimization at every level.
Stack frame layout. C functions often have local variables whose addresses are taken and passed around. That pointer needs to point somewhere valid for the function's entire lifetime. If you reuse the stack frame for a tail call, the old function's locals vanish, but something might still hold a pointer to them. This is undefined behavior territory, and C compilers tend to be conservative about UB. Modern systems languages like Rust solve similar pointer lifetime issues with concepts like the borrow checker, which can be explored in our guide to Rust ownership and the borrow checker.
Debugging. If the compiler reuses stack frames, your debugger shows a flat call stack even when the actual call chain is deep. GDB, LLDB, and every other C debugger assumes stack frames map 1:1 to function calls. Breakpoints in tail-called functions get weird. Stack traces lie to you. Profilers get confused. Compiler engineers spent years trying to make tail-call optimization play nicely with -g debug info, and the results were never great.
ABI compatibility. On x86-64, the System V ABI specifies exactly how functions set up and tear down their stack frames. Tail-call optimization sometimes requires different register allocation or stack cleanup sequences. If your optimized code calls a shared library that wasn't optimized the same way, things break in subtle, architecture-specific ways. The compiler can optimize internal calls safely, but it can't always guarantee correctness across translation unit boundaries.
alloca and VLAs. C lets you allocate memory on the stack dynamically. This completely breaks tail-call reuse because the stack pointer isn't where the callee expects it. Any function containing alloca or variable-length arrays is ineligible, and checking for these adds complexity to the optimizer.
Each of these problems is solvable. But stacked together, they represented years of engineering work that nobody was willing to fund.
The Committee Politics
The C standards committee (ISO/IEC JTC1/SC22/WG14) operates by consensus. That means proposals need broad support, and broad support means addressing every concern from every major compiler vendor.
GCC, Clang, Microsoft's MSVC, and the embedded compilers (IAR, Green Hills, various vendor-specific ones) all have different optimization pipelines. A tail-call optimization standard needs to be specific enough to be useful but flexible enough that every compiler can implement it without rewriting their backend.
For years, proposals bounced between "must perform TCO" (like Scheme) and "may perform TCO" (which compilers could already do as an optimization). The first camp argued that without a guarantee, you can't write portable recursive code that won't blow the stack. The second camp argued that guarantees create portability traps since not every compiler can deliver on every platform.
There's also a cultural divide. Some committee members come from the embedded systems world, where stack usage is audited line-by-line and any invisible optimization is suspect. Others come from compiler research, where TCO is a solved problem that's been working in LLVM for over a decade. These groups don't always speak the same language.
And honestly, C had other fires to put out. C11 added atomics and threads. C23 cleaned up decades of accumulated weirdness. Tail-call optimization kept getting bumped because it was seen as a quality-of-life thing, not a correctness thing.
What Finally Changed
A few things converged in the early 2020s.
First, security researchers started publishing real-world exploits where deep recursion in C code caused stack overflows that opened vulnerabilities. Buffer overflows get all the attention, but stack exhaustion bugs were becoming a serious attack surface, especially in parsers and protocol handlers. Making tail recursion safe stopped being academic.
Second, the WebAssembly crowd applied pressure. Wasm's execution model is naturally friendly to tail calls, and Wasm-targeting C compilers wanted a standard way to emit them. If you're compiling C to Wasm, you need TCO to avoid blowing up the tiny default stack. Setting up reproducible toolchains for such specialized targets is a challenge, which is why solutions like reproducible AI workstations using NixOS are gaining traction.
Third, LLVM and GCC had independently matured their TCO passes to handle most of the edge cases. The engineering work was largely done. What was missing was the standardization glue that lets programmers write portable code.
The C2y working draft (what people are calling C26, though the numbering isn't final) includes [[musttail]] as an attribute. The syntax signals the compiler: "I've verified this call site is safe for tail-call reuse. If you can't optimize it, error out." This is the right approach. It puts the burden of verification on the programmer (who knows the code's constraints) and gives the compiler a hard guarantee it can trust.
int process(int state, int input) {
// ... computation ...
[[musttail]] return next_state(new_state, new_input);
}If the compiler can't do the optimization, you get a compile-time error, not silent stack growth. That's a big deal. It means you can rely on the optimization instead of hoping.
Why "Musttail" Instead of Implicit Optimization
Some people wanted the compiler to figure it out automatically, like GCC's -foptimize-sibling-calls flag has tried to do for years. But implicit optimization creates a trap. You write code that works great on GCC with -O2, ship it, and then someone compiles it with a different compiler or a different optimization level and the stack overflows in production. Silent optimization that sometimes works is worse than no optimization at all.
[[musttail]] makes the contract explicit. The programmer says "this is a tail call," the compiler says "confirmed" or "nope, can't do it here." No ambiguity. No platform-specific surprises.
It also sidesteps the ABI problem. If a call site has [[musttail]], the compiler knows it can safely reuse the frame. If it doesn't have the attribute, the compiler behaves exactly as it always has. No existing code breaks.
What This Means for C Developers
If you write recursive C code today, you're probably already managing stack depth manually or using trampolines. The new attribute doesn't change your existing workflow overnight.
But it opens up patterns that were previously impractical. State machines expressed as mutually recursive functions. Parser combinators that don't blow the stack on deep input. Recursive descent algorithms that were rejected in favor of iterative versions purely for performance reasons.
And for the people compiling C to WebAssembly or running on embedded devices with 4KB stacks, [[musttail]] is a genuine quality-of-life upgrade they've been waiting for.



