Your PHP Container Metrics Are Lying to You
Most monitoring tools report host resources instead of container limits. When your container has 512MB but the host has 64GB, your memory usage graph shows 0.7% instead of the real 85%.
A few months ago I was debugging an OOM kill in a Kubernetes pod running PHP-FPM. The monitoring dashboard showed memory usage at 3.2%. The pod had been killed twice in the last hour. How can a process be killed for exceeding memory limits when the graph says it's using 3.2%?
The answer is painfully simple: the monitoring library was reading /proc/meminfo, which reports the host node's memory. The node had 64GB of RAM. The pod's resource limit was 512MB. The actual container memory usage was 87%, not 3.2%. The dashboard was lying.
Why This Happens
On Linux, /proc/meminfo is a kernel-generated pseudo-file that reports system-wide memory statistics. When you run cat /proc/meminfo inside a Docker container, you see the host's total memory, not the container's cgroup limit. This is a well-known quirk of Linux containers.
Most PHP monitoring libraries read system metrics like this:
// This reports HOST memory, not container memory
$meminfo = file_get_contents('/proc/meminfo');
preg_match('/MemTotal:\s+(\d+)/', $meminfo, $matches);
$totalKb = (int) $matches[1]; // Returns 64GB on a 512MB container
The same problem applies to CPU metrics. /proc/cpuinfo shows all host CPUs, even if your container is limited to 0.5 cores via cpu.cfs_quota_us.
cgroup v1 vs cgroup v2
Container resource limits are enforced through Linux cgroups (control groups). There are two versions in active use, and they store limit information in different locations.
cgroup v1 (older kernels, EKS default until recently):
# Memory limit
/sys/fs/cgroup/memory/memory.limit_in_bytes
# Memory usage
/sys/fs/cgroup/memory/memory.usage_in_bytes
# CPU quota (microseconds per period)
/sys/fs/cgroup/cpu/cpu.cfs_quota_us
/sys/fs/cgroup/cpu/cpu.cfs_period_us
cgroup v2 (newer kernels, default on Ubuntu 22.04+, GKE):
# Memory limit ("max" means unlimited)
/sys/fs/cgroup/memory.max
# Memory usage
/sys/fs/cgroup/memory.current
# CPU quota (format: "quota period")
/sys/fs/cgroup/cpu.max
The first challenge is detecting which version you are running under. The second is handling edge cases: memory.limit_in_bytes on cgroup v1 returns 9223372036854771712 (a huge number) when no limit's set, while cgroup v2's memory.max returns the string max.
The Real Numbers
Here is a comparison of what most PHP monitoring tools report versus the actual container state:
Metric |
| cgroup (Correct) |
|---|---|---|
Total Memory | 64 GB (host) | 512 MB (container limit) |
Used Memory | 2.1 GB (host) | 437 MB (container) |
Usage % | 3.2% | 85.4% |
Available | 61.9 GB | 75 MB |
The difference isn't academic. When your autoscaler reads 3.2% memory usage, it doesn't scale up. When your alerting checks threshold at 80%, it never fires. Meanwhile your pod is being OOM-killed every 45 minutes.
CPU metrics are equally misleading. A container with cpu.cfs_quota_us = 50000 and cpu.cfs_period_us = 100000 has 0.5 CPU cores available. But /proc/cpuinfo shows all 32 host cores, and a naive CPU usage calculation against those 32 cores will never show meaningful numbers.
How System Metrics Fixes This
System Metrics detects the container environment automatically and reads the correct source for each metric:
use Cbox\SystemMetrics\SystemMetrics;
$container = SystemMetrics::container()->getValue();
if ($container->isContainer()) {
echo "Memory limit: " . $container->memoryLimitBytes . " bytes\n";
echo "Memory usage: " . $container->memoryUsageBytes . " bytes\n";
echo "Utilization: " . round($container->memoryUtilizationPercentage(), 1) . "%\n";
echo "CPU cores: " . $container->availableCpuCores() . "\n";
}
The detection logic works in three steps:
Check for cgroup v2 by looking for
/sys/fs/cgroup/cgroup.controllersFall back to cgroup v1 by checking
/sys/fs/cgroup/memory/memory.limit_in_bytesValidate limits are real by filtering out the "no limit" sentinel values
When a container has no explicit limit set, System Metrics reports the host values but flags hasMemoryLimit() as false, so your code can distinguish between "limited to 512MB" and "unlimited, showing host values."
What You Should Do
If you are running PHP in containers, audit your monitoring stack. Check whether your dashboards show host metrics or container metrics. The easiest test:
// If this returns more memory than your container limit,
// your metrics library is reading /proc/meminfo
$total = SystemMetrics::memory()->getValue()->totalBytes;
echo round($total / 1024 / 1024) . " MB\n";
Compare that number with your container's resource limit in your Kubernetes manifest or Docker Compose file. If they don't match, you have been monitoring the wrong numbers.
The full container metrics API, including cgroup v1/v2 detection, CPU quota parsing, and memory limit validation, is documented in the System Metrics docs.
// Sylvester Damgaard