# Understanding Memory Management and Ballooning Limitations in Container VMs

> Learn about container VM memory management and ballooning limitations in macOS Virtualization. Discover how freed guest memory isn't returned to the host until VM restart.

- Repository: [Apple/container](https://github.com/apple/container)
- Tags: deep-dive
- Published: 2026-06-29

---

**Container VMs allocate memory statically up to a user-defined limit, but macOS Virtualization framework only supports partial ballooning—freed guest memory is not returned to the host until the VM restarts.**

The `apple/container` project runs each container inside its own lightweight Linux virtual machine built on Apple’s Virtualization framework. While this provides strong isolation and a native Linux environment, the memory management model differs significantly from traditional containers or full virtualization. Understanding how `container` translates CLI flags into VM configuration—and why ballooning limitations prevent automatic host memory reclamation—is essential for managing resource-intensive workloads on macOS.

## How Container VMs Allocate Memory

### Static VM Memory Limits

When you create a container, the `--memory` flag sets a hard upper bound for the entire VM. For example, `container run --memory 8g` configures the VM to never exceed 8 GiB regardless of host availability. This value is parsed by `Parser.memoryStringAsMiB` in [`Sources/Services/ContainerAPIService/Client/Parser.swift`](https://github.com/apple/container/blob/main/Sources/Services/ContainerAPIService/Client/Parser.swift) and stored as `resources.memoryInBytes` in the container configuration. The [`MachinesService.swift`](https://github.com/apple/container/blob/main/MachinesService.swift) file then passes this value to the VM launch command as `bootConfig.memory`.

### Dynamic Usage Inside the Guest

The Linux kernel inside the VM commits only the pages that processes actually touch. Consequently, a container started with `--memory 16g` may show only a few gigabytes of RAM in macOS Activity Monitor if the workload is idle. However, this "unused" portion remains reserved by the VM and is not available to other host processes until the VM stops.

## The Ballooning Limitation Explained

### Partial Ballooning Support

According to [`docs/technical-overview.md`](https://github.com/apple/container/blob/main/docs/technical-overview.md), macOS Virtualization framework implements only a **partial** memory-ballooning mechanism. While the VM can shrink its footprint under certain conditions, **pages that the guest frees are not returned to macOS**. The host observes the memory high-water mark until the VM itself is stopped and restarted. This behavior is explicitly documented in the "Releasing container memory to macOS" section of the technical overview.

### Why Memory Stays Committed

This limitation means that even if a container's processes free memory aggressively using `free()` or drop caches, the underlying VM retains those pages from the host perspective. The [`docs/technical-overview.md`](https://github.com/apple/container/blob/main/docs/technical-overview.md) file notes that the only reliable way to reclaim that memory from the host is to stop the affected container(s) or the entire builder VM and start them again.

## Practical Impact on Host Resources

### Cumulative Memory Pressure

Running multiple memory-intensive containers can cause the host's RAM usage to grow continuously, even when individual containers have freed most of their memory internally. Users monitoring `container stats` will see low `memoryUsageBytes` compared to `memoryLimitBytes` (as reported by [`ContainerStats.swift`](https://github.com/apple/container/blob/main/ContainerStats.swift)), but Activity Monitor still shows high VM memory consumption because the freed pages remain assigned to the VM.

### The Restart Requirement

To actually release physical pages back to macOS, you must stop the VM. For persistent development machines, update the configuration and restart:

```bash
container machine set -n dev memory=12G
container machine stop dev
container machine run -n dev -- nproc

```

## Configuring Memory Limits and Defaults

### Default Memory Allocation

If you omit the `--memory` flag, `container` defaults to **half of the host's RAM**. This calculation resides in [`Sources/ContainerPersistence/ContainerSystemConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPersistence/ContainerSystemConfig.swift) where `defaultMemory` is derived from the host's total physical memory. The builder VM used for `container build` defaults to 2 GiB unless explicitly configured via `container builder start --memory`.

### Parsing Memory Strings

The CLI accepts human-readable sizes like "8g" or "512M" through the `memoryStringAsMiB` method:

```swift
// Sources/Services/ContainerAPIService/Client/Parser.swift
public static func memoryStringAsMiB(_ memory: String) throws -> Int64 {
    let ram = try Measurement.parse(parsing: memory)
    return Int64(ram.converted(to: .mebibytes).value)
}

```

### Builder VM Configuration

For build operations that require more RAM, increase the builder memory before starting:

```bash
container builder start --memory 16g
container build .

```

## Monitoring Memory Usage at Runtime

### Runtime Statistics

The [`Sources/ContainerCommands/Container/ContainerStats.swift`](https://github.com/apple/container/blob/main/Sources/ContainerCommands/Container/ContainerStats.swift) file reports both `memoryUsageBytes` and `memoryLimitBytes` to the CLI. The `container stats` command displays current utilization against the configured limit, formatted as:

```swift
let memUsageStr = stats2.memoryUsageBytes.map { Self.formatBytes($0) } ?? notAvailable
let memLimitStr = stats2.memoryLimitBytes.map { Self.formatBytes($0) } ?? notAvailable
print("Memory: \(memUsageStr) / \(memLimitStr)")

```

### Inspecting Configuration

To view the exact memory limit for a running container in bytes:

```bash
container inspect my-container --format json | jq '.configuration.resources.memoryInBytes'

```

## Summary

- **Static allocation**: The `--memory` flag sets a hard VM limit at creation time, parsed by `Parser.memoryStringAsMiB` and stored in `resources.memoryInBytes`.
- **Lazy commit**: The Linux guest only touches pages it needs, but unused capacity remains reserved from the host perspective.
- **Ballooning limits**: macOS Virtualization framework does not return freed guest pages to the host; the high-water mark persists until VM restart.
- **Default behavior**: Without explicit flags, containers use ½ host RAM (configured in [`ContainerSystemConfig.swift`](https://github.com/apple/container/blob/main/ContainerSystemConfig.swift)), while builder VMs default to 2 GiB.
- **Reclamation**: Stop and restart containers or the builder VM to free memory back to macOS.

## Frequently Asked Questions

### Why does Activity Monitor show high memory usage after my container freed memory inside the VM?

This occurs because of the partial ballooning limitation in macOS Virtualization framework. While the Linux guest may have freed pages internally, those pages are not returned to the host operating system. The host sees the memory high-water mark until you stop and restart the container VM, as documented in [`docs/technical-overview.md`](https://github.com/apple/container/blob/main/docs/technical-overview.md).

### How do I reclaim memory from a running container without stopping it?

You cannot reclaim memory from the host without stopping the VM. According to the `apple/container` source code, there is no mechanism to return freed guest pages to macOS while the VM is running. You must execute `container machine stop` for persistent machines or stop the ephemeral container, then restart to release the physical RAM.

### What is the default memory limit if I do not specify the `--memory` flag?

The default is **half of the host's physical RAM**. This calculation occurs in [`Sources/ContainerPersistence/ContainerSystemConfig.swift`](https://github.com/apple/container/blob/main/Sources/ContainerPersistence/ContainerSystemConfig.swift) where the `defaultMemory` property divides the host's total memory by two. For builder VMs used with `container build`, the default is specifically 2 GiB unless overridden with `container builder start --memory`.

### How do I permanently change the memory allocation for a development machine?

Use the `container machine set` command to update the configuration, then restart the machine. For example, `container machine set -n dev memory=12G` updates the persistent configuration, but the change only takes effect after you run `container machine stop dev` followed by `container machine run -n dev`. This process is detailed in [`docs/container-machine.md`](https://github.com/apple/container/blob/main/docs/container-machine.md).