DSA Tutorial
🔍

Call Stack

What Is the Call Stack?

The call stack is a region of memory the operating system allocates for a program to manage function calls. It is a LIFO stack — the most recently called function is always at the top and is the one currently executing.

MEMORY LAYOUT of a typical process:

High addresses  ┌────────────────────────┐
                │     STACK              │  ← grows downward
                │  (function call frames)│
                ├────────────────────────┤
                │         ↓             │  stack growth direction
                │                        │
                │         ↑             │  heap growth direction
                ├────────────────────────┤
                │     HEAP               │  ← grows upward
                │  (malloc/new memory)   │
                ├────────────────────────┤
                │     DATA SEGMENT       │  global/static variables
                ├────────────────────────┤
Low addresses   │     TEXT SEGMENT       │  program instructions (code)
                └────────────────────────┘

Default stack sizes:
  Linux:   8 MB   (ulimit -s 8192)
  macOS:   8 MB   (main thread), 512 KB (other threads)
  Windows: 1 MB   (default), configurable
  JVM:     256 KB – 1 MB per thread (configurable with -Xss)

A single recursive call frame is typically 50–200 bytes.
At 100 bytes/frame: 8 MB stack → max ~80,000 simultaneous frames.

Anatomy of a Stack Frame

Each function call pushes one frame onto the stack. The frame holds everything that function needs to execute independently:

Stack frame for factorial(n):

  ┌──────────────────────────────────────────┐
  │ [HIGH ADDRESS]                           │
  │ Saved registers (rbx, rbp, r12...)       │ ← CPU state to restore on return
  │ Return address                           │ ← instruction to resume in caller
  │ Parameters: n (copy of argument)         │ ← passed from caller
  │ Local variables: result, tmp...          │ ← declared in function body
  │ [LOW ADDRESS]                            │
  └──────────────────────────────────────────┘
  
  Stack pointer (SP) points to the top of the current frame.
  Frame pointer (FP/RBP) points to the base of the current frame.
  The difference gives the frame size.

For factorial(5), five frames on the stack simultaneously:

  [High address]
  ┌──────────────┐  ← SP (top)
  │ factorial(1) │  n=1, return_addr=factorial+offset
  ├──────────────┤
  │ factorial(2) │  n=2, return_addr=factorial+offset
  ├──────────────┤
  │ factorial(3) │  n=3, return_addr=factorial+offset
  ├──────────────┤
  │ factorial(4) │  n=4, return_addr=factorial+offset
  ├──────────────┤
  │ factorial(5) │  n=5, return_addr=main+offset
  ├──────────────┤
  │ main()       │  ← FP (frame pointer for main)
  └──────────────┘
  [Low address]

Stack Depth Limits Per Language

Java
1public class StackLimits { 2 3 // Test: how deep can Java recurse? 4 static int maxDepth = 0; 5 6 public static void measureDepth(int depth) { 7 try { 8 maxDepth = depth; 9 measureDepth(depth + 1); 10 } catch (StackOverflowError e) { 11 System.out.println("Java StackOverflowError at depth: " + maxDepth); 12 } 13 } 14 15 // Increase stack size with: java -Xss16m StackLimits 16 public static void main(String[] args) { 17 measureDepth(0); 18 // Default JVM: ~5,000–10,000 depending on frame size and -Xss setting 19 // With -Xss16m: ~50,000–100,000 20 21 // Actual StackOverflowError example 22 try { 23 infiniteRecursion(); 24 } catch (StackOverflowError e) { 25 System.out.println("Caught StackOverflowError: " + e.getClass().getName()); 26 } 27 } 28 29 static void infiniteRecursion() { 30 infiniteRecursion(); // No base case — guaranteed overflow 31 } 32}
Output (approximate):
Java:       StackOverflowError at depth ~5,000-10,000
Python:     Default recursion limit: 1000; Actual max depth: ~990
C++:        Segmentation fault (no exception — undefined behaviour)
JavaScript: RangeError: Maximum call stack size exceeded (~10,000-15,000)

Stack Overflow: Causes and Diagnosis

CAUSE 1: Infinite recursion (no base case or unreachable base case)

  def f(n):                    def f(n):
      return f(n - 1)              if n < 0: return 0  ← unreachable for positive n
  ← No base case                   return f(n + 1)     ← wrong direction: n grows!

CAUSE 2: Base case that is never reached due to wrong input

  def factorial(n):
      if n == 0: return 1
      return n * factorial(n - 1)

  factorial(-1):
    n=-1 ≠ 0 → factorial(-2) → factorial(-3) → ... → stack overflow
  Fix: guard with n < 0 check or assert n >= 0

CAUSE 3: Correct recursion but n is too large

  factorial(100_000)  ← 100,000 frames × ~100 bytes = 10 MB
                         exceeds default 8 MB stack → overflow
  Fix: iterative version, or increase stack size

DIAGNOSIS CHECKLIST:
  □ Does every path reach the base case?
  □ Does the recursive call's argument always move toward the base case?
  □ Is n small enough that the recursion depth fits in available stack?
  □ For n > 10,000: consider iterative or tail-recursive version
Java
1public class StackOverflowDiagnosis { 2 3 // BAD: no base case 4 static int bad1(int n) { 5 return bad1(n - 1); // ← StackOverflowError 6 } 7 8 // BAD: base case unreachable for negative input 9 static int bad2(int n) { 10 if (n == 0) return 0; 11 return bad2(n - 1); // ← overflow for n = -1 12 } 13 14 // GOOD: guard against invalid input 15 static int good(int n) { 16 if (n < 0) throw new IllegalArgumentException("n must be >= 0"); 17 if (n == 0) return 0; 18 return n + good(n - 1); 19 } 20 21 public static void main(String[] args) { 22 // Try bad2 with negative input 23 try { 24 bad2(-1); 25 } catch (StackOverflowError e) { 26 System.out.println("bad2(-1): StackOverflowError (n never reaches 0)"); 27 } 28 29 // Good version guards correctly 30 try { 31 good(-1); 32 } catch (IllegalArgumentException e) { 33 System.out.println("good(-1): " + e.getMessage()); 34 } 35 System.out.println("good(5): " + good(5)); // 15 36 } 37}

Tail Call Optimisation (TCO)

A tail call is a recursive call that is the very last operation in a function — the result is returned directly without any further computation.

NOT a tail call:
  return n * factorial(n - 1)
         ↑ pending multiplication after recursive call returns

IS a tail call:
  return factTail(n - 1, n * acc)
         ↑ return value is the recursive call's result, nothing more

With TCO, the compiler/runtime reuses the SAME frame instead of pushing a new one:

Without TCO (standard recursion):
  [main] [f(5)] [f(4)] [f(3)] [f(2)] [f(1)] [f(0)]   ← 7 frames

With TCO (tail-recursive):
  [main] [f]     ← single frame, reused 6 times
  Each call overwrites the current frame's parameters with the new arguments.

Languages with guaranteed TCO:
  Scheme, Haskell, Scala (tail calls in tail position)
  JavaScript ES6+    (required by spec, but V8 does NOT implement it)
  C (some compilers optimise tail calls in -O2)

Languages WITHOUT guaranteed TCO:
  Java   ← no TCO; each call creates a new frame
  Python ← no TCO; Guido van Rossum explicitly rejected it
  C++    ← compiler may optimise tail calls but NOT guaranteed
  JavaScript (V8) ← spec says yes, V8 has not implemented it
Java
1public class TailCallOptimisation { 2 3 // NOT tail-recursive: pending multiplication n × result 4 public static long factNormal(int n) { 5 if (n == 0) return 1; 6 return n * factNormal(n - 1); // ← pending operation after call 7 } 8 9 // TAIL-RECURSIVE: accumulator carries the partial result 10 // The recursive call is the LAST thing that happens 11 public static long factTail(int n, long acc) { 12 if (n == 0) return acc; // Base: return accumulated result 13 return factTail(n - 1, acc * n); // ← last operation, no pending work 14 } 15 16 // Convenience wrapper 17 public static long factorial(int n) { 18 return factTail(n, 1); // Start accumulator at 1 19 } 20 21 // Java doesn't apply TCO — same stack depth either way. 22 // But tail-recursive form is ready to be converted to iteration: 23 public static long factIterative(int n) { 24 long acc = 1; 25 while (n > 0) { // Loop ≡ tail recursion 26 acc *= n; 27 n--; 28 } 29 return acc; 30 } 31 32 public static void main(String[] args) { 33 System.out.println(factNormal(10)); // 3628800 34 System.out.println(factorial(10)); // 3628800 35 System.out.println(factIterative(10)); // 3628800 36 } 37}
Output:
3628800
3628800
3628800

Converting Deep Recursion to Explicit Stack

When recursion depth is too large for the system call stack, convert to iteration using an explicit stack (heap-allocated data structure). The heap can hold gigabytes — the call stack only megabytes.

Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class ExplicitStack { 5 6 // Recursive DFS — can overflow for deep trees 7 public static void dfsRecursive(int[][] adj, int start, boolean[] visited) { 8 visited[start] = true; 9 System.out.print(start + " "); 10 11 for (int neighbour : adj[start]) { 12 if (!visited[neighbour]) { 13 dfsRecursive(adj, neighbour, visited); 14 } 15 } 16 } 17 18 // Iterative DFS — uses explicit stack (heap memory, no overflow risk) 19 public static void dfsIterative(int[][] adj, int start, int n) { 20 boolean[] visited = new boolean[n]; 21 Deque<Integer> stack = new ArrayDeque<>(); 22 23 stack.push(start); 24 25 while (!stack.isEmpty()) { 26 int node = stack.pop(); 27 28 if (visited[node]) continue; 29 visited[node] = true; 30 System.out.print(node + " "); 31 32 // Push neighbours in reverse order to maintain left-to-right traversal 33 for (int i = adj[node].length - 1; i >= 0; i--) { 34 if (!visited[adj[node][i]]) { 35 stack.push(adj[node][i]); 36 } 37 } 38 } 39 } 40 41 public static void main(String[] args) { 42 // Graph: 0-1-2-3-4 (path graph) 43 int[][] adj = {{1}, {0, 2}, {1, 3}, {2, 4}, {3}}; 44 45 System.out.print("Recursive DFS: "); 46 boolean[] visited = new boolean[5]; 47 dfsRecursive(adj, 0, visited); 48 System.out.println(); 49 50 System.out.print("Iterative DFS: "); 51 dfsIterative(adj, 0, 5); 52 System.out.println(); 53 } 54}
Output:
Recursive DFS: 0 1 2 3 4
Iterative DFS: 0 1 2 3 4

How to Convert Any Recursion to Explicit Stack

Pattern: every recursive function can be converted by simulating
what the call stack does automatically.

RECURSIVE VERSION:
  def solve(args):
      if base_case(args): return base_result
      left  = solve(smaller_args_1)
      right = solve(smaller_args_2)
      return combine(left, right)

EXPLICIT STACK VERSION:
  def solve_iterative(initial_args):
      stack   = [(initial_args, "call")]
      results = {}

      while stack:
          args, phase = stack.pop()

          if phase == "call":
              if base_case(args):
                  results[args] = base_result
              else:
                  # Push the "combine" phase to run AFTER both sub-calls
                  stack.append((args, "combine"))
                  # Push sub-calls (right first so left runs first via LIFO)
                  stack.append((smaller_args_2, "call"))
                  stack.append((smaller_args_1, "call"))

          elif phase == "combine":
              results[args] = combine(results[smaller_args_1],
                                      results[smaller_args_2])

      return results[initial_args]

For simple LINEAR recursion (one call, result used directly):
  The conversion is simpler — just replace recursion with a loop.
  See factIterative from the TCO section above.

Stack Frame Size — Why It Matters

Larger frames → fewer possible recursion levels for the same stack size.

Example comparison:
  Function A: int n, int result         → ~8 bytes on stack
  Function B: int arr[1000], int n      → ~4008 bytes on stack

  8 MB stack:
    Function A: 8,000,000 / 8     = 1,000,000 max levels
    Function B: 8,000,000 / 4008  = ~2,000 max levels

MINIMISE frame size by:
  ✓ Passing large data structures by reference (pointer), not by value
  ✓ Not declaring large local arrays inside recursive functions
  ✓ Using global or heap-allocated buffers for large data

C++ specific:
  void sort(int arr[], int n) {  ← arr is a pointer (8 bytes) — good
  void sort(int arr[1000]) {     ← arr is a value copy (4000 bytes) — BAD for recursion

Language-Specific Stack Information

JAVA:
  Default stack: -Xss (default 512KB on 64-bit JVM, varies by version)
  Increase:      java -Xss4m MyProgram   (4 MB stack)
  Exception:     java.lang.StackOverflowError
  No TCO:        each call creates a frame

PYTHON:
  Default limit: sys.getrecursionlimit() == 1000
  Increase:      sys.setrecursionlimit(10000)
  Exception:     RecursionError: maximum recursion depth exceeded
  No TCO:        Guido explicitly rejected it; use iteration for deep recursion

C++:
  Default stack: OS-dependent (Linux: 8MB, Windows: 1MB)
  Increase:      ulimit -s unlimited  (Linux), or linker flags
  No exception:  segmentation fault — undefined behaviour, may be silent
  Partial TCO:   compiler may optimise tail calls at -O2 but not guaranteed

JAVASCRIPT (V8/Node.js):
  Default:       ~10,000–15,000 frames
  Exception:     RangeError: Maximum call stack size exceeded
  No TCO:        V8 has not implemented ES6's required TCO (as of 2024)
  Node.js --stack-size: node --stack-size=65536 app.js  (in KB)

Common Mistakes

Ignoring stack overflow for large inputs. A function that works perfectly for n=100 may crash with n=10,000. Always consider whether your recursive algorithm's depth (O(n) for linear, O(log n) for divide-and-conquer) combined with the input size could exceed the stack limit. Convert to iterative for n > ~10,000 in Java/C++, n > ~900 in Python.

Declaring large local arrays in recursive functions. Each recursive frame allocates space for all local variables. A char buffer[4096] inside a recursive function uses 4 KB per frame — 100 levels deep means 400 KB just for buffers, easily exhausting the stack. Pass large buffers as parameters by reference/pointer, or allocate them once on the heap before the recursion starts.

Relying on TCO in Java or Python. Both languages lack tail call optimisation. A tail-recursive function in Java or Python still creates a new stack frame for every call. Converting a tail-recursive function to iterative (while loop with accumulator) is the correct fix for deep recursion in these languages.

Catching StackOverflowError and continuing. In Java, StackOverflowError is an Error (not Exception) and indicates the JVM is in an undefined state. Catching it and continuing is extremely risky — the stack may be corrupted. The correct response is to fix the recursion depth issue, not to catch the error.

Interview Questions

Q: What is a stack overflow and how do you fix it?

A stack overflow occurs when recursive calls accumulate more stack frames than the allocated stack size allows. Each frame stores parameters, local variables, and the return address — typically 50-200 bytes. With a default 1-8 MB stack, you can have roughly 5,000-50,000 simultaneous frames. Fixes: (1) add or fix the base case to ensure the recursion terminates; (2) fix the recursive case to move toward the base case; (3) convert deep linear recursion to iteration; (4) for divide-and-conquer, reduce frame size by passing references instead of copies; (5) increase stack size (JVM -Xss, Python sys.setrecursionlimit).

Q: What is tail call optimisation and which languages support it?

TCO reuses the current stack frame when a recursive call is in "tail position" — the call's result is returned directly with no pending computation. Instead of pushing a new frame, the runtime overwrites the current frame's parameters and jumps back to the function's start. This converts O(n) stack space to O(1). Languages with guaranteed TCO: Scheme, Haskell, Scala. Languages without: Java (no TCO), Python (explicitly rejected), V8/JavaScript (required by ES6 spec but not implemented in major engines as of 2024), C++ (compiler may optimise but not guaranteed).

Q: How do you convert a recursive algorithm to use an explicit stack?

Replace the call stack with an explicit stack data structure (array, deque) on the heap. Each element on the explicit stack represents what the call stack would have held: the function's arguments and the "phase" of execution (pre-call or post-call for the combine step). The key difference: the explicit stack lives on the heap and can grow to gigabytes, while the call stack is limited to megabytes. The algorithm is logically identical — only the memory location of the stack changes.

FAQs

Why does Python have such a low default recursion limit of 1000?

Python's recursion limit is conservatively low for two reasons: (1) Python's stack frames are large — each frame holds the full Python frame object including code object references, local variable dict, and more — consuming significantly more memory per frame than C or Java frames. (2) The limit catches infinite recursion quickly, preventing long hangs. The limit can be raised with sys.setrecursionlimit(), but the better solution for algorithms needing depth > 1000 is to write an iterative version.

Is the call stack the same as the stack data structure?

They are the same concept — a LIFO (Last In, First Out) structure. The call stack is a specific implementation of a stack that the OS manages automatically for function calls. When you implement an explicit stack using Deque or list, you are creating the same LIFO structure in heap memory. The algorithms are identical — the difference is only where the memory lives and who manages it.

Can the stack and heap overlap and cause silent corruption?

Yes — this is a serious problem in C/C++. The stack grows downward and the heap grows upward in typical memory layouts. If a deeply recursive program exhausts the stack, the stack pointer can cross into heap memory, corrupting heap-allocated data silently before a segfault occurs. In Java, Python, and JavaScript, the runtime protects against this with checked limits (StackOverflowError, RecursionError, RangeError). In C/C++, there is no protection — hence the "undefined behaviour" classification.

Summary

The call stack is a fixed-size region of memory (typically 1-8 MB) where function call frames are pushed and popped in LIFO order.

Each stack frame stores:

  • Parameters (own copies)
  • Local variables
  • Return address (where the caller resumes)
  • Saved CPU registers

Stack limits per language:

  • Java: ~5,000–10,000 frames (default), configurable with -Xss
  • Python: 1,000 calls (default), configurable with sys.setrecursionlimit()
  • C++: ~10,000–80,000 frames (OS stack size / frame size), no exception on overflow
  • JavaScript (V8): ~10,000–15,000 frames, RangeError on overflow

Tail call optimisation: reuses the current frame for a tail-position call — O(1) stack space instead of O(n). Guaranteed in Scheme/Haskell. NOT guaranteed in Java, Python, or V8 JavaScript.

Converting to explicit stack: replace the system call stack with a heap-allocated Deque/list. Heap memory can reach gigabytes — the call stack is limited to megabytes. The algorithm logic is identical; only the stack's memory location changes.

Key rules:

  • Always ensure the base case is reachable for all valid inputs
  • Ensure every recursive call moves strictly toward the base case
  • For depth > ~1,000 in Python or > ~10,000 in Java/JS: consider iterative
  • Do not declare large arrays as local variables in recursive functions
  • Catch StackOverflowError only to diagnose — never to continue silently

In the next topic, you will explore Base Case and Recursive Case — the formal structure of every recursive function, how to identify and design base cases, and how to verify the recursive case makes progress.

Suggested Quiz

What is stored in a single stack frame for a function call?

1/6
Call Stack