Back to Apache Flink
Q30Question 30 of 33

What is Flink's memory model for TaskManagers and how do you tune it

Key Concepts
  • taskmanager.memory.process.size is the total memory budget for the TaskManager process and must match the container or pod limit exactly.
  • JVM heap is split into framework heap (Flink runtime) and task heap (user code objects and on-heap state).
  • Managed memory is off-heap memory used by the RocksDB state backend's block cache and write buffers, and by batch sorters and hash tables.
  • Network memory is a pool of off-heap buffers for the shuffle exchange between tasks across the network.
  • taskmanager.memory.managed.fraction (default ~0.4) controls RocksDB state performance; raise it for large-state jobs.
  • taskmanager.memory.network.fraction (default ~0.1) sizes the shuffle buffer pool; too little causes insufficient-network-buffers errors.
  • Prefer tuning fractions over pinning absolute sizes because fractions adapt when you resize the container.

Definition

A TaskManager's process size splits into JVM heap, managed memory for RocksDB and sorting, network buffers for shuffles, and metaspace plus overhead, tuned by setting process size and adjusting managed and network fractions.
  • Process size – The total memory the TaskManager process is allowed to use, set by taskmanager.memory.process.size, which must match the container limit.
  • JVM heap – Split into framework heap (Flink runtime objects) and task heap (user code objects and on-heap state).
  • Managed memory – Off-heap memory managed by Flink, used by the RocksDB state backend's block cache and write buffers, and by batch sorting and hashing.
  • Network memory – A pool of off-heap buffers used for shuffle exchange between tasks across the network.
  • Metaspace and overhead – JVM metaspace for class metadata and a safety overhead reserve for thread stacks and native usage.
  • Fraction tuning – taskmanager.memory.managed.fraction and taskmanager.memory.network.fraction shift the balance between regions.

Think of a TaskManager as one bank branch building at the National Bank of India with a fixed total floor space (process size). That space is divided into rooms: offices for staff and their paperwork (heap), a large secure vault for the RocksDB ledgers and sorting workbenches (managed memory), a loading dock with conveyor belts for moving parcels between branches (network buffers), and a maintenance closet (metaspace and overhead). If the vault is too small the ledgers overflow; if the loading dock is too small parcels back up. Tuning is deciding how much floor space each room gets.

Understanding the Concepts

What is the TaskManager memory model?

The top-level knob is taskmanager.memory.process.size, the total memory the TaskManager process may use. This must match the container or pod limit exactly. Flink derives all sub-regions from this total.

TEXT
# flink-conf.yaml
taskmanager.memory.process.size: 4096m  # match pod limit

# Flink derives from this:
#   JVM Metaspace:   256m (default)
#   JVM Overhead:    ~10% of total
#   Flink Total Memory = process.size - metaspace - overhead
#     Framework Heap:  128m
#     Task Heap:       derived (remainder)
#     Managed Memory:  40% of Flink Total (default)
#     Network Memory:  10% of Flink Total (default)
#     Task Off-Heap:   0 (default)
JAVA
package com.bank.config;

public class MemoryModelBreakdown {
    public static void main(String[] args) {
        long processSize = 4096; // MB
        long metaspace = 256;    // MB (default)
        long overhead = (long)(processSize * 0.1); // ~410 MB
        long flinkTotal = processSize - metaspace - overhead;
        // flinkTotal = 4096 - 256 - 410 = 3430 MB

        long managedMemory = (long)(flinkTotal * 0.4); // 1372 MB
        long networkMemory = (long)(flinkTotal * 0.1); // 343 MB
        long frameworkHeap = 128; // MB (default)
        long taskHeap = flinkTotal - managedMemory
            - networkMemory - frameworkHeap; // ~1587 MB

        System.out.println("Process size: " + processSize + " MB");
        System.out.println("Managed (RocksDB): " + managedMemory + " MB");
        System.out.println("Network (shuffle): " + networkMemory + " MB");
        System.out.println("Task heap (user): " + taskHeap + " MB");
    }
}

What is managed memory and when do you tune it?

Managed memory is off-heap memory that Flink manages directly. The RocksDB state backend uses it for block cache and write buffers. Batch execution uses it for sorters and hash tables. The fraction defaults to 0.4.

TEXT
# Streaming job with large RocksDB state:
# Raise managed fraction so RocksDB has enough block cache
taskmanager.memory.managed.fraction: 0.6

# Streaming job with heap state backend (no RocksDB):
# Lower managed fraction since RocksDB is not used
taskmanager.memory.managed.fraction: 0.2

# Batch job with heavy sorting:
# Keep managed fraction high for sorters
taskmanager.memory.managed.fraction: 0.5
JAVA
package com.bank.config;

import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class ManagedMemoryDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
            StreamExecutionEnvironment.getExecutionEnvironment();

        // RocksDB state backend: uses managed memory
        env.setStateBackend(new EmbeddedRocksDBStateBackend(true));

        // Flink bounds RocksDB to its managed memory share
        // state.backend.rocksdb.memory.managed: true (default)
        // If managed memory is too small:
        //   - RocksDB thrashes disk (block cache misses)
        //   - State access slows dramatically
        //   - Checkpoint size grows
        // Fix: raise taskmanager.memory.managed.fraction

        env.execute("NBI Managed Memory Demo");
    }
}

What is network memory and when do you tune it?

Network memory is a pool of off-heap buffers for the shuffle exchange between tasks. The fraction defaults to 0.1 with configurable min and max bounds. The classic symptom of too little is an insufficient-network-buffers error at startup.

TEXT
# Default network memory configuration:
taskmanager.memory.network.fraction: 0.1
taskmanager.memory.network.min: 64mb
taskmanager.memory.network.max: 1gb

# Job with wide shuffles and high parallelism:
# Needs more network memory
taskmanager.memory.network.fraction: 0.15

# Error when network memory is too small:
# java.io.IOException:
#   Insufficient number of network buffers:
#   required 512, but only 256 available
JAVA
package com.bank.config;

public class NetworkMemoryTuning {
    // Network buffers scale with:
    //   parallelism x number of shuffle connections
    //
    // A job at parallelism 4 with 2 shuffles:
    //   ~128 buffers needed
    //
    // Same job scaled to parallelism 32 with 2 shuffles:
    //   ~1024 buffers needed
    //
    // If you scale parallelism without increasing network memory,
    // you get insufficient-network-buffers at startup
    //
    // Fix: raise taskmanager.memory.network.fraction or .max
}

How do the memory regions map to a 4GB container?

| Region | Size (approx) | Purpose |
|---|---|---|
| JVM Metaspace | 256 MB | Class metadata |
| JVM Overhead | 410 MB | Thread stacks, native allocations |
| Framework Heap | 128 MB | Flink runtime objects |
| Task Heap | 1587 MB | User code, on-heap state |
| Managed Memory | 1372 MB | RocksDB block cache, batch sorters |
| Network Memory | 343 MB | Shuffle buffers |
| Total | 4096 MB | Matches container limit |

JAVA
package com.bank.config;

public class MemoryLayout {
    public static void main(String[] args) {
        // 4GB container on Kubernetes
        // flink-conf.yaml:
        //   taskmanager.memory.process.size: 4096m
        //   taskmanager.memory.managed.fraction: 0.4
        //   taskmanager.memory.network.fraction: 0.1
        //
        // Kubernetes pod spec:
        //   resources:
        //     limits:
        //       memory: 4096Mi  <-- MUST match process.size
        //
        // If process.size > pod limit: OOMKilled by Kubernetes
        // If process.size < pod limit: wasted memory
    }
}

What are common mistakes with memory tuning?

The most common mistake is not matching process.size to the container limit. Setting it too high gets the container killed for out-of-memory (OOMKilled). Setting it too low wastes allocated pod memory.

TEXT
# WRONG: process.size > pod limit
taskmanager.memory.process.size: 6144m
# Pod limit: 4096Mi
# Result: OOMKilled by Kubernetes!

# WRONG: process.size < pod limit
taskmanager.memory.process.size: 2048m
# Pod limit: 4096Mi
# Result: 2GB wasted, RocksDB starved

# CORRECT: process.size = pod limit
taskmanager.memory.process.size: 4096m
# Pod limit: 4096Mi
# Result: full utilization, no OOM risk
JAVA
// WRONG: pinning absolute sizes that don't adapt
taskmanager.memory.managed.size: 1024m
taskmanager.memory.network.size: 256m
// When you resize the container, these don't scale!

// CORRECT: use fractions that adapt to container size
taskmanager.memory.managed.fraction: 0.4
taskmanager.memory.network.fraction: 0.1
// Resize container from 4GB to 8GB: fractions auto-scale

How does RocksDB use managed memory?

When state.backend.rocksdb.memory.managed is true (default), Flink allocates RocksDB's block cache and write buffers from managed memory. This prevents RocksDB from using unbounded native memory.

JAVA
package com.bank.config;

import org.apache.flink.contrib.streaming.state.EmbeddedRocksDBStateBackend;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class RocksDbMemoryDemo {
    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env =
            StreamExecutionEnvironment.getExecutionEnvironment();

        EmbeddedRocksDBStateBackend rocksdb =
            new EmbeddedRocksDBStateBackend(true); // incremental
        env.setStateBackend(rocksdb);

        // RocksDB memory is bounded by managed memory:
        // state.backend.rocksdb.memory.managed: true (default)
        //
        // With managed.fraction=0.4 and 4GB container:
        //   RocksDB gets ~1.4GB for block cache + write buffers
        //
        // If this is too small:
        //   - Block cache hit rate drops
        //   - RocksDB reads from disk more often
        //   - State access latency spikes
        //   - Fix: increase managed.fraction

        env.execute("NBI RocksDB Memory Demo");
    }
}

What about JVM metaspace?

Metaspace holds class metadata. Jobs that generate or load many classes (heavy code generation, many UDFs) can exhaust metaspace. The default is 256 MB. Metaspace exhaustion is a distinct failure from heap exhaustion.

TEXT
# Default metaspace:
taskmanager.memory.jvm-metaspace.size: 256m

# If you see: java.lang.OutOfMemoryError: Metaspace
# Increase metaspace:
taskmanager.memory.jvm-metaspace.size: 512m

# Common causes:
#   - Many user-defined functions (UDFs)
#   - Heavy Flink SQL code generation
#   - Class loading from many jars

What should a senior engineer know about memory tuning?

A senior engineer should know that process.size is the hard ceiling that must match the container, that managed memory is the single biggest lever for RocksDB performance, that network memory scales with parallelism, and that they should prefer fractions over absolute sizes for adaptability.

TEXT
Senior memory tuning checklist:
  1. Set process.size = container/pod memory limit
  2. For RocksDB jobs: raise managed.fraction (0.5-0.6)
  3. For heap-state jobs: lower managed.fraction (0.2)
  4. For high-parallelism shuffles: raise network.fraction (0.15)
  5. Monitor: RocksDB block cache hit rate, checkpoint size
  6. Monitor: network buffer usage, insufficient-buffer errors
  7. Watch metaspace for heavy code-generation workloads
  8. Use fractions, not absolute sizes, for adaptability
  9. Test memory config under peak load before production

Visual Overview

DIAGRAM
  TaskManager Memory Model (4 GB Container)

  ┌──────────────────────────────────────────────┐
  │        taskmanager.memory.process.size: 4GB    │
  │                                                │
  │  ┌──────────────────────┐                      │
  │  │   JVM Metaspace       │  256 MB              │
  │  │   (class metadata)    │                      │
  │  └──────────────────────┘                      │
  │  ┌──────────────────────┐                      │
  │  │   JVM Overhead        │  ~410 MB             │
  │  │   (threads, native)   │                      │
  │  └──────────────────────┘                      │
  │  ┌──────────────────────────────────────────┐  │
  │  │           Flink Total Memory (~3430 MB)   │  │
  │  │                                            │  │
  │  │  ┌────────────────┐ ┌──────────────────┐  │  │
  │  │  │ Framework Heap │ │   Task Heap       │  │  │
  │  │  │   128 MB       │ │   ~1587 MB        │  │  │
  │  │  │ (Flink runtime)│ │ (user code, state)│  │  │
  │  │  └────────────────┘ └──────────────────┘  │  │
  │  │  ┌──────────────────────────────────────┐  │  │
  │  │  │       Managed Memory  ~1372 MB        │  │  │
  │  │  │  (RocksDB block cache, write buffers  │  │  │
  │  │  │   batch sorters, hash tables)          │  │  │
  │  │  └──────────────────────────────────────┘  │  │
  │  │  ┌──────────────────────────────────────┐  │  │
  │  │  │       Network Memory   ~343 MB        │  │  │
  │  │  │  (shuffle buffers between tasks)       │  │  │
  │  │  └──────────────────────────────────────┘  │  │
  │  └──────────────────────────────────────────┘  │
  └──────────────────────────────────────────────┘
       Must equal container/pod memory limit!

Key Takeaway

A Flink TaskManager's memory is not one undivided heap; it is a carefully partitioned process budget. The top-level knob is taskmanager.memory.process.size, the total the process may use, which must match the container or pod limit exactly. From that, Flink reserves JVM metaspace and overhead, then divides the rest into task heap for user objects and on-heap state, managed memory for RocksDB's block cache and batch sorters, and network memory for shuffle buffers.

Tuning is about matching region sizes to the workload. For a RocksDB-heavy streaming job, raise the managed fraction so RocksDB has enough block cache to avoid constantly hitting disk. For high-parallelism shuffles, raise the network fraction to prevent insufficient-buffer errors at startup. The symptom of starving managed memory is slow state access and large checkpoints; the symptom of starving network memory is an error at startup.

At the National Bank of India, our stateful job kept months of per-account aggregates in RocksDB. Under load it slowed badly because RocksDB was starved of block cache. Raising the managed fraction gave RocksDB enough off-heap cache and the slowdown disappeared without changing the 4 GB container limit. Later, when we scaled parallelism for peak volume, it failed at startup with an insufficient-network-buffers error, fixed by raising the network fraction.

The discipline is always the same: set process.size equal to the pod limit and shift the managed and network fractions to fit the workload.

Remember one sentence: Set process.size to the container limit and tune managed.fraction for RocksDB state performance and network.fraction for shuffle buffer capacity.

---

Share
Use ← → arrow keys to navigate