DATAS
Runtime adaptation to allocation rate and concurrency pressure (.NET 9+)
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:
Allocation Budget Based on LDS: Sets maximum allocations before next GC based on live data size (not throughput)
Throughput-Aware Actual Budget: While the maximum is based on LDS, the actual budget considers throughput to maintain performance
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)
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 π―
DATAS = Heap Size Proportional to App Needs: The core goal is keeping your heap size proportional to your Live Data Size, not maximizing throughput.
Default in .NET 9: If you're on .NET 9+, you're already using DATAS. No configuration needed.
Best for Bursty Workloads: DATAS shines when your application's memory needs vary over timeβit grows and shrinks the heap accordingly.
More GCs, Less Memory: Expect higher Gen0/Gen1 collection counts but significantly lower memory usage (80%+ reduction in benchmarks).
Dynamic Heap Count: DATAS starts with 1 heap and scales up/down based on allocation pressureβa hybrid between Workstation and Server GC.
Container-Friendly: In memory-constrained environments, DATAS helps fit more processes and prevents unexpected memory growth.
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
Microsoft Documentation - DATAS: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/datas - Official documentation with benchmark results
Maoni Stephens' Blog: https://maoni0.medium.com/dynamically-adapting-to-application-sizes-2d72fcb6f1ea - Deep dive from the GC architect who designed DATAS
.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. π