Diagnosing and Fixing Space Leaks in Haskell Applications
Learn how to identify and resolve space leaks in Haskell by diagnosing thunk build-up using GHC profiling tools and applying strictness annotations.
07 Oct 2025, 00:25 UTC

The Symptom: Memory Growth Without a Leak
A Haskell application experiencing a space leak does not suffer from "lost" memory in the C-style sense. Instead, it suffers from thunk build-up. A thunk is a deferred computation—a promise to calculate a value later. When these promises accumulate in long-lived data structures without being evaluated, they consume heap space and can eventually trigger an OutOfMemory exception or cause severe garbage collection (GC) pauses.
Quick Diagnostic Table
| Observation | Likely Cause | Diagnostic Tool |
|---|---|---|
| Linear memory growth over time | Accumulation of thunks in a state loop or fold | +RTS -s |
| Sudden spike during final result output | Deeply nested thunk chain (Stack Overflow) | -prof (Heap Profile) |
| High GC overhead, low productivity | Large numbers of small, short-lived thunks | eventlog2html |
Step-by-Step Diagnostic Process
To resolve a space leak, you must first identify whether the leak is caused by a lazy accumulator or a lazy data structure.
1. Baseline Memory Analysis
Compile your application with -rtsopts to enable Runtime System options. Run the executable with the -s flag to see the GC statistics:
# Run on the compiled binary
./my-app +RTS -s
What to look for: Check the "Total memory in use" and the percentage of time spent in GC. If GC time exceeds 20-30% and memory usage grows steadily without plateauing, you likely have a space leak.
2. Heap Profiling
If the baseline confirms growth, enable profiling to see which constructors are filling the heap. Compile with -prof -fprof-auto and run with -hc (cost-center profiling):
# Run with heap profiling enabled
./my-app +RTS -hc -p
This generates a .hp file. Convert this to a readable format using hp2ps or eventlog2html. Look for a "mountain" shape in the graph; the label on that mountain identifies the function or data type causing the growth.
Common Fixes Based on Findings
Finding: Lazy Accumulators in Folds
A common culprit is foldl. Because it is lazy, it builds a massive chain of thunks before evaluating anything.
The Fix: Replace foldl with foldl' (the strict version) from Data.List. This forces the accumulator to be evaluated at each step.
Finding: Lazy Fields in Records
If your heap profile shows a specific record type growing, the fields may be lazy, allowing thunks to accumulate inside the record.
The Fix: Use Strictness Annotations (Bang Patterns). By adding a ! to a field, you force it to be evaluated to Weak Head Normal Form (WHNF) when the record is created.
-- Before: Lazy fields
data AppState = AppState { count :: Int, total :: Double }
-- After: Strict fields
data AppState = AppState { count :: !Int, total :: !Double }
Finding: Nested Data Structure Leaks
Sometimes seq or bang patterns are insufficient because they only evaluate the outermost constructor (WHNF). If you have a list of lists, a bang pattern on the outer list still leaves the inner lists as thunks.
The Fix: Use the deepseq library to force evaluation to Normal Form (fully evaluated).
import Control.DeepSeq
-- Force evaluation of the entire structure before proceeding
let result = force (complexCalculation input)
in result `seq` nextStep result
Verification and Limitations
To verify the fix, rerun the application with +RTS -s. The "Total memory in use" should now plateau or remain constant relative to the input size, and GC productivity should increase.
Practical Limitations
- Performance Trade-off: Forcing strictness everywhere can eliminate the benefits of lazy evaluation (e.g., short-circuiting or infinite streams), potentially slowing down the application.
- Bottom Values: Changing a lazy field to a strict field will cause a runtime exception if the value is
undefined(bottom), whereas a lazy field would have simply deferred the crash. - WHNF vs NF: Always remember that
seqonly evaluates to the first constructor. For complex trees or lists,deepseqis required.
Escalation Criteria
If memory growth persists after applying foldl' and strict record fields, escalate to the following:
- Retainer Profiling: Use
-hrto see why an object is being held in memory rather than what the object is. - GHC Eventlog: Use
-landeventlog2htmlto analyze GC pauses and heap residency in real-time. - Custom Memory Management: Consider
Compact Regionsfor very large, long-lived data sets to reduce GC overhead.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.