Avoiding Space Leaks in Haskell: When to Prefer foldl' Over foldl
Lazy foldl can build a huge chain of thunks, causing space leaks. Use foldl’ and strictness annotations to keep memory constant, and verify with GHC’s +RTS -s runtime statistics.
28 Jun 2026, 19:22 UTC

Problem: Lazy foldl Builds Thunks That Grow With the List
Haskell’s default evaluation strategy is lazy. When you write a simple numeric sum like
sum [1..10^8], the compiler expands sum into a left fold:
foldl (+) 0 [1..10^8]
This foldl is lazy – it does not evaluate the accumulator until the end of the list. Each step creates a new thunk that holds the previous accumulator and the next element to add. After processing a million items, you have a chain of a million thunks. The garbage collector cannot reclaim any of them because they are all still referenced by the final thunk. The result is a space leak that can exhaust the heap.
Solution: Use the Strict Foldl’ (foldl’)
GHC’s Data.List module (and modern Prelude) exports a strict variant called foldl’. It forces the accumulator to weak head normal form (WHNF) on every iteration, preventing the chain of thunks from growing. For numeric accumulators like Int or Integer this guarantees constant‑space behaviour:
import Data.List (foldl')
largeSum :: Int
largeSum = foldl' (+) 0 [1..10^8]
Running this with +RTS -s shows a maximum residency of a few megabytes and a total allocation close to the size of the list, regardless of list length.
Verifying Heap Behaviour With GHC Runtime Statistics
To confirm that a fold is constant‑space, compile the program with no optimisations and run it with the runtime statistics flag:
ghc -O0 -rtsopts -with-rtsopts=-s Sum.hs
./Sum +RTS -s -RTS
The -s flag prints a summary at program exit:
Allocated: 400,000,000 bytes, Live: 10,000,000 bytes, Max residency: 15,000,000 bytes
For foldl you’ll see a Max residency that grows with the list, while foldl’ stays flat. Repeat at -O2 – the optimizer’s strictness analysis may eliminate the leak, so you can compare the two builds to see if the optimisation is doing the heavy lifting.
When Strictness Isn’t Enough: Nested Lazy Structures
foldl’ only forces the accumulator to WHNF. If the accumulator itself contains lazy fields, the leak can persist. For example:
data Counters = Counters { c1 :: Int, c2 :: Int }
sumCounters :: [Counters] -> Counters
sumCounters = foldl' (
(Counters a b) (Counters x y) -> Counters (a + x) (b + y)
) (Counters 0 0)
Here foldl’ keeps the accumulator in WHNF, but each Int field is lazy. The chain of thunks for c1 and c2 still grows. Adding BangPatterns to the accumulator’s fields or using a StrictData extension solves the problem:
data Counters = Counters { c1 :: !Int, c2 :: !Int }
sumCounters = foldl' (
(Counters a b) (Counters x y) -> Counters (a + x) (b + y)
) (Counters 0 0)
Now the accumulator is fully strict, and the runtime statistics confirm constant‑space behaviour again.
Trade‑Offs and Practical Guidance
- Performance vs. Laziness:
foldlis lazy and can be useful when you need to compose pipelines that produce infinite lists or when you want to defer computation. Use it only when you’re sure the accumulator won’t grow. - Compiler Optimisation: GHC’s strictness analysis can automatically transform
foldlinto a constant‑space version at-O1or-O2for simple patterns. Relying on this can be risky; always test with-O0and-sto catch leaks in unoptimised builds or in GHCi. - Explicit Strictness: For performance‑critical modules, enable
BangPatternsorStrictDataglobally to make all fields strict by default. This reduces the chance of accidental leaks. - Space‑Leak Detection: Run your program with
+RTS -s -sstderrduring CI to surface unexpected memory growth. - Large Numbers: When summing
Integervalues,foldl’still keeps the accumulator strict but the underlying representation may allocate more memory as the number grows. MonitorLivebytes in the stats to detect runaway growth.
Actionable Closing
When writing Haskell code that processes large or streaming data:
- Choose
foldl’for numeric or strictly‑structured accumulators. - Use BangPatterns or StrictData for nested lazy fields.
- Verify constant‑space behaviour with
+RTS -sat-O0and-O2. - Add runtime statistics to your CI pipeline to catch regressions.
By following these steps you avoid the classic space leak of lazy foldl while still enjoying Haskell’s compositional elegance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.