DATAS

Runtime adaptation to allocation rate and concurrency pressure (.NET 9+)

Lesson 15 of 26 available0 practice questions

Last generated

DATAS: Dynamic Adaptation To Application Sizes

Understanding garbage collection in .NET becomes significantly more powerful when you grasp how DATAS (Dynamic Adaptation To Application Sizes) optimizes memory management based on your application's actual memory needs. This lesson covers heap size adaptation, live data size tracking, and workload-responsive memory managementβ€”essential concepts for building efficient .NET applications in memory-constrained environments.

Welcome to Dynamic Adaptation To Application Sizes πŸ’»

Welcome to one of the most impactful GC features in modern .NET! DATAS represents a fundamental shift in how the garbage collector manages memory. Instead of aggressively growing the heap based on throughput (like traditional Server GC), DATAS aims to keep your heap size proportional to your application's Live Data Size (LDS)β€”the actual memory your application needs.

Think of it as a smart thermostat for memory: instead of always running at full blast, it adjusts based on what you actually need, saving resources when demand is low and scaling up when necessary.

DATAS was introduced as an opt-in feature in .NET 8 and became enabled by default in .NET 9. If you're running .NET 9+, you're already benefiting from DATAS.

Core Concepts: The Mechanics of DATAS πŸ”§

What is DATAS?

DATAS (Dynamic Adaptation To Application Sizes) is a GC feature that adapts heap size to match your application's memory requirements. The core principle: your heap size should be roughly proportional to your long-lived data size.

Key characteristics:

  • Size adaptation: Heap grows and shrinks based on actual memory needs
  • Workload-aware: Responds to bursty workloads by expanding during load and contracting during idle periods
  • Consistent sizing: Same application doing same work has similar heap size across different hardware
  • Memory efficiency: Particularly valuable in containerized and memory-constrained environments

πŸ’‘ Tip: DATAS is enabled by default in .NET 9+. You can disable it by setting DOTNET_GCDynamicAdaptationMode=0 if needed.

The Problem DATAS Solves

Before DATAS, Server GC optimized for throughput and treated the process as dominant on the machine:

Traditional Server GC Behavior

  Light Load:
    Heap: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (400MB)
    Actual need: β–ˆβ–ˆ (40MB)
    ❌ Heap doesn't shrink when workload decreases

  Heavy Load:
    Heap: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (800MB)
    Actual need: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (400MB)
    ❌ Heap grows aggressively, never gives memory back

  Different Hardware (same workload):
    Machine A (8 cores):  Heap 500MB
    Machine B (48 cores): Heap 2GB
    ❌ Same work, vastly different memory usage

With DATAS:

DATAS Behavior

  Light Load:
    Heap: β–ˆβ–ˆβ–ˆβ–ˆ (80MB)
    Actual need: β–ˆβ–ˆ (40MB)
    βœ… Heap stays proportional to live data

  Heavy Load:
    Heap: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (500MB)
    Actual need: β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ (400MB)
    βœ… Heap expands to accommodate, then contracts

  Different Hardware (same workload):
    Machine A (8 cores):  Heap ~200MB
    Machine B (48 cores): Heap ~200MB
    βœ… Consistent sizing regardless of hardware

What is Live Data Size (LDS)?

Live Data Size is the central metric DATAS optimizes around. It represents:

  • The amount of memory your application would use after the most aggressive GC possible
  • Long-lived data + any in-flight data present during a GC
  • Essentially: "How much memory does your app actually need?"
Concept Description Example
Long-lived Data Objects that survive many GC cycles Caches, static data, connection pools
In-flight Data Temporary objects alive during GC Request objects, temporary buffers
Live Data Size Long-lived + In-flight at GC time What DATAS uses to size the heap

How DATAS Works

DATAS achieves size adaptation through several mechanisms:

  1. Allocation Budget Based on LDS: Sets maximum allocations before next GC based on live data size (not throughput)

  2. Throughput-Aware Actual Budget: While the maximum is based on LDS, the actual budget considers throughput to maintain performance

  3. Dynamic Heap Count: Starts with one heap and grows/shrinks the number of heaps as neededβ€”a hybrid between Workstation GC (1 heap) and full Server GC (1 heap per core)

  4. Compacting GCs: Performs full compacting collections when needed to prevent fragmentation

DATAS Decision Flow

  After GC:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Measure Live Data Size (LDS)            β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Calculate max allocation budget         β”‚
  β”‚ based on LDS                            β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
                      β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Adjust actual budget for throughput     β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β”‚
          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
          β–Ό                       β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Workload      β”‚       β”‚ Workload      β”‚
  β”‚ increasing?   β”‚       β”‚ decreasing?   β”‚
  β”‚               β”‚       β”‚               β”‚
  β”‚ Add heaps     β”‚       β”‚ Remove heaps  β”‚
  β”‚ if needed     β”‚       β”‚ Compact more  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

DATAS vs Traditional Server GC

Aspect Traditional Server GC DATAS
Optimization Goal Maximize throughput Heap size proportional to app needs
Heap Growth Aggressive if memory available Conservative, based on LDS
Heap Shrinking Rarely shrinks Actively shrinks when workload decreases
Number of Heaps Fixed (1 per core) Dynamic (1 to core count)
Cross-machine Consistency Varies greatly with hardware Similar size for same workload
GC Frequency Lower (larger generations) Higher Gen0/Gen1 collections
Best For Throughput-critical, memory-abundant Bursty workloads, containers, multi-tenant

When DATAS Helps Most

DATAS provides the biggest benefits for:

Bursty Workloads: Applications with variable load patterns where memory needs fluctuate significantly.

Memory-Constrained Environments: Containers, cloud instances, and multi-tenant servers where fitting more processes matters.

Capacity Planning: When you need predictable memory usage regardless of hardware specs.

Cost Optimization: Cloud environments where you pay for memoryβ€”smaller heaps mean lower costs.

Practical Examples πŸ› οΈ

Example 1: Observing DATAS in Action

Monitor memory behavior with and without DATAS:

public class DatasDemo
{
    public static void Main()
    {
        Console.WriteLine($".NET Version: {Environment.Version}");
        
        // Simulate bursty workload
        for (int burst = 0; burst < 5; burst++)
        {
            Console.WriteLine($"\n--- Burst {burst + 1} ---");
            
            // Allocate heavily
            var data = new List<byte[]>();
            for (int i = 0; i < 1000; i++)
            {
                data.Add(new byte[10240]); // 10KB each = 10MB total
            }
            
            var info = GC.GetGCMemoryInfo();
            Console.WriteLine($"After allocation - Heap: {info.HeapSizeBytes / 1024 / 1024}MB");
            
            // Release and let GC run
            data.Clear();
            data = null;
            GC.Collect(2, GCCollectionMode.Aggressive);
            GC.WaitForPendingFinalizers();
            
            info = GC.GetGCMemoryInfo();
            Console.WriteLine($"After GC - Heap: {info.HeapSizeBytes / 1024 / 1024}MB");
            Console.WriteLine($"Committed: {info.TotalCommittedBytes / 1024 / 1024}MB");
            
            // Idle period
            Thread.Sleep(1000);
        }
    }
}

With DATAS (.NET 9 default): You'll see the heap shrink back after each burst.

Without DATAS: The heap tends to stay at peak size.

Example 2: Comparing Memory Usage

// Run same workload, compare memory on different configs

// Config 1: DATAS enabled (default in .NET 9)
// DOTNET_GCDynamicAdaptationMode=1

// Config 2: DATAS disabled
// DOTNET_GCDynamicAdaptationMode=0

public class MemoryComparison
{
    public static async Task RunWorkload()
    {
        var cache = new Dictionary<int, byte[]>();
        
        // Simulate web server workload
        for (int request = 0; request < 10000; request++)
        {
            // Each "request" allocates temporary data
            var requestData = new byte[1024 * 100]; // 100KB per request
            ProcessRequest(requestData);
            
            // Some requests add to cache (long-lived)
            if (request % 100 == 0)
            {
                cache[request] = new byte[1024 * 10]; // 10KB cached
            }
            
            if (request % 1000 == 0)
            {
                var info = GC.GetGCMemoryInfo();
                Console.WriteLine($"Request {request}: Heap={info.HeapSizeBytes / 1024 / 1024}MB, " +
                                  $"Gen0={GC.CollectionCount(0)}, Gen1={GC.CollectionCount(1)}, Gen2={GC.CollectionCount(2)}");
            }
        }
    }
    
    static void ProcessRequest(byte[] data)
    {
        // Simulate work
        Array.Fill(data, (byte)42);
    }
}

Expected Results:

Metric DATAS Disabled DATAS Enabled
Peak Heap ~500MB ~150MB
Gen0/Gen1 GCs Lower Significantly higher
Throughput Slightly higher 2-3% lower
Working Set Large 80%+ smaller

Example 3: Container-Optimized Configuration

For containerized applications, DATAS shines:

// runtimeconfig.json
{
  "configProperties": {
    "System.GC.Server": true,
    "System.GC.Concurrent": true,
    // DATAS is enabled by default in .NET 9
    // Explicit setting for documentation:
    "System.GC.DynamicAdaptationMode": 1,
    // Set hard limit to container memory limit (minus buffer)
    "System.GC.HeapHardLimit": 419430400  // 400MB
  }
}
## Dockerfile
FROM mcr.microsoft.com/dotnet/aspnet:9.0

## Container with 512MB limit
## DATAS will keep heap proportional to actual needs
## HeapHardLimit prevents OOM kills

ENV DOTNET_GCHeapHardLimit=419430400

COPY ./publish /app
WORKDIR /app
ENTRYPOINT ["dotnet", "MyApp.dll"]

Example 4: Monitoring DATAS Decisions

Use GC.GetGCMemoryInfo() to observe DATAS behavior:

public class DatasMonitor
{
    public static void PrintGCInfo()
    {
        var info = GC.GetGCMemoryInfo();
        
        Console.WriteLine("=== GC Memory Info ===");
        Console.WriteLine($"Heap Size: {info.HeapSizeBytes / 1024 / 1024} MB");
        Console.WriteLine($"Committed: {info.TotalCommittedBytes / 1024 / 1024} MB");
        Console.WriteLine($"Fragmented: {info.FragmentedBytes / 1024 / 1024} MB");
        Console.WriteLine($"Promoted: {info.PromotedBytes / 1024 / 1024} MB");
        Console.WriteLine($"Compacted: {info.Compacted}");
        Console.WriteLine($"Concurrent: {info.Concurrent}");
        
        // Generation sizes
        for (int gen = 0; gen <= 2; gen++)
        {
            var genInfo = info.GenerationInfo[gen];
            Console.WriteLine($"Gen{gen}: Size={genInfo.SizeAfterBytes / 1024}KB, " +
                              $"Fragmentation={genInfo.FragmentationAfterBytes / 1024}KB");
        }
        
        // Pause durations
        foreach (var pause in info.PauseDurations)
        {
            Console.WriteLine($"Pause: {pause.TotalMilliseconds:F2}ms");
        }
    }
}

Common Mistakes ⚠️

Mistake 1: Disabling DATAS Without Measuring

❌ Wrong approach:

{
  "System.GC.DynamicAdaptationMode": 0
}
// "Server GC is faster, so I'll disable DATAS"

βœ… Correct approach:

// Measure first, then decide
// Run benchmarks with both settings
// DATAS enabled (default)
// vs
// DATAS disabled: DOTNET_GCDynamicAdaptationMode=0

// Compare:
// - Memory usage (working set, committed)
// - Throughput (requests/sec, items processed)
// - Latency (p50, p95, p99)

Why: DATAS trades 2-3% throughput for 80%+ memory reduction. In many environments (containers, cloud, multi-tenant), memory savings far outweigh the small throughput cost.

Mistake 2: Expecting Immediate Heap Shrinking

❌ Wrong expectation:

// "I released objects but heap didn't shrink immediately!"
data = null;
GC.Collect();
// Heap still large...

βœ… Correct understanding:

// DATAS shrinks heap over time through:
// 1. More frequent Gen0/Gen1 collections
// 2. Compacting Gen2 collections when fragmentation is high
// 3. Reducing heap count when load decreases

// This is gradual, not instant
// Monitor over time, not immediately after one GC

Why: DATAS is adaptive, not reactive. It takes several GC cycles to measure the new workload pattern and adjust accordingly.

Mistake 3: Confusing Heap Count with Thread Count

❌ Wrong mental model:

"DATAS adjusts GC thread count dynamically"

βœ… Correct mental model:

"DATAS adjusts heap count dynamically"
- Starts with 1 heap (like Workstation GC)
- Can grow up to 1 heap per core (like Server GC)
- More heaps = more parallelism for allocations
- Fewer heaps = less memory overhead

Why: Each heap has its own allocator. More heaps reduce allocation contention but increase memory overhead. DATAS finds the right balance.

Mistake 4: Using DATAS for Throughput-Critical Batch Processing

❌ Wrong use case:

// Batch processing millions of records
// Where every millisecond counts
// DATAS will collect more frequently, reducing throughput

βœ… Right approach:

// For throughput-critical batch processing:
// Consider disabling DATAS
Environment.SetEnvironmentVariable("DOTNET_GCDynamicAdaptationMode", "0");

// Or use latency mode to reduce GC interference
GCSettings.LatencyMode = GCLatencyMode.Batch;

Why: DATAS optimizes for memory efficiency, not maximum throughput. If you have abundant memory and need peak throughput, traditional Server GC may be better.

Key Takeaways 🎯

  1. DATAS = Heap Size Proportional to App Needs: The core goal is keeping your heap size proportional to your Live Data Size, not maximizing throughput.

  2. Default in .NET 9: If you're on .NET 9+, you're already using DATAS. No configuration needed.

  3. Best for Bursty Workloads: DATAS shines when your application's memory needs vary over timeβ€”it grows and shrinks the heap accordingly.

  4. More GCs, Less Memory: Expect higher Gen0/Gen1 collection counts but significantly lower memory usage (80%+ reduction in benchmarks).

  5. Dynamic Heap Count: DATAS starts with 1 heap and scales up/down based on allocation pressureβ€”a hybrid between Workstation and Server GC.

  6. Container-Friendly: In memory-constrained environments, DATAS helps fit more processes and prevents unexpected memory growth.

  7. Measure Before Disabling: The 2-3% throughput cost is often worth the memory savings. Benchmark your specific workload before disabling.

πŸ“‹ Quick Reference Card

πŸ“‹ DATAS Quick Reference

Aspect Key Information
Full Name Dynamic Adaptation To Application Sizes
Availability Opt-in .NET 8, Default .NET 9+
Core Metric Live Data Size (LDS) - long-lived + in-flight data
Goal Heap size proportional to application memory needs
Mechanisms β€’ LDS-based allocation budget
β€’ Dynamic heap count (1 to core count)
β€’ Compacting GCs for fragmentation
Trade-offs 2-3% throughput reduction for 80%+ memory savings
Best For Containers, bursty workloads, multi-tenant, capacity planning
Configuration DOTNET_GCDynamicAdaptationMode: 0=Disabled, 1=Enabled
Monitoring GC.GetGCMemoryInfo(), dotnet-counters, ETW events

Memory Device: DATAS = Data Adapts The Application's Size

  • Detect live data size
  • Adjust heap proportionally
  • Tune heap count dynamically
  • Actively compact when needed
  • Shrink when workload decreases

🧠 Mental Model: DATAS as a Smart Storage Unit

Think of DATAS like a smart storage unit for your belongings:

  • Traditional Server GC: Rents the biggest unit available and never downsizes, even when you remove items
  • DATAS: Monitors what you actually store, moves to a smaller unit when you have less stuff, expands when you need more

The smart unit costs slightly more per move (more GC cycles), but you pay much less in rent (memory usage).

πŸ“š Further Study

  1. Microsoft Documentation - DATAS: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/datas - Official documentation with benchmark results

  2. Maoni Stephens' Blog: https://maoni0.medium.com/dynamically-adapting-to-application-sizes-2d72fcb6f1ea - Deep dive from the GC architect who designed DATAS

  3. .NET 9 Runtime Release Notes: https://github.com/dotnet/core/blob/main/release-notes/9.0/preview/preview7/runtime.md - Details on DATAS becoming default

Congratulations! You now understand how DATAS dynamically adapts .NET heap size to your application's actual memory needs. This knowledge helps you build memory-efficient applications, especially in containerized and cloud environments. πŸš€