Configuring Memory Limits for Systemd-Managed Services: Preventing OOM with cgroups


2 views

When working with systemd services, traditional ulimit approaches often prove insufficient because:

  • Systemd launches processes in its own execution context
  • Child processes may inherit or override limits
  • OOM killer behavior is unpredictable

The modern solution lies in systemd's integration with cgroups. Add these directives to your service unit file (/etc/systemd/system/your-service.service):

[Service]
MemoryMax=500M
MemoryHigh=450M
MemorySwapMax=100M

Key parameters explanation:

Directive Effect
MemoryMax Hard limit - triggers OOM killer when exceeded
MemoryHigh Soft limit - throttles allocations when approached
MemorySwapMax Controls swap usage separately

For a memory-intensive Python service, here's a complete unit file:

[Unit]
Description=Memory-limited data processor
After=network.target

[Service]
Type=simple
User=appuser
ExecStart=/usr/bin/python3 /opt/app/main.py
MemoryMax=1G
MemoryHigh=900M
MemorySwapMax=200M
Restart=on-failure

[Install]
WantedBy=multi-user.target

For more granular control, consider these additional settings:

MemoryLimit=800M
AllowedCPUs=0-3
IOWeight=50

After modifying your service file:

sudo systemctl daemon-reload
sudo systemctl restart your-service
systemd-analyze verify your-service.service  # Check for syntax errors
systemctl show your-service | grep Memory   # View applied limits

Configure custom behavior when limits are hit:

[Service]
...
OOMPolicy=kill
OOMScoreAdjust=-100

For systems without recent systemd versions, manually configure via:

sudo cgcreate -g memory:/your-service
echo "500000000" > /sys/fs/cgroup/memory/your-service/memory.limit_in_bytes
echo "appuser" > /sys/fs/cgroup/memory/your-service/cgroup.procs

Remember that modern systemd versions (v230+) provide better integration than manual cgroup management.


When dealing with systemd services, traditional ulimit approaches often fall short because:

  • Systemd launches processes in its own context
  • Child processes may inherit different limits
  • OOM killer behavior is unpredictable

The modern solution uses cgroups via systemd's native directives. Add these to your service unit file (/etc/systemd/system/your-service.service):

[Service]
MemoryMax=500M
MemoryHigh=450M
MemorySwapMax=100M

Key parameters:

  • MemoryMax: Hard limit (process gets killed when exceeded)
  • MemoryHigh: Soft limit (throttling occurs)
  • MemorySwapMax: Swap space limitation

For a Node.js application running as a systemd service:

[Unit]
Description=Node.js Memory Limited Service

[Service]
ExecStart=/usr/bin/node /opt/app/server.js
User=appuser
Group=appgroup
MemoryMax=1.5G
MemoryHigh=1.2G
MemorySwapMax=0
Restart=on-failure

[Install]
WantedBy=multi-user.target

After applying changes (systemctl daemon-reload and systemctl restart your-service), verify with:

systemd-cgtop -m
# Or for specific service:
systemctl show your-service -p MemoryCurrent,MemoryMax

For more granular control, create a slice with memory limits:

[Unit]
Description=Memory Limited Slice

[Slice]
MemoryMax=2G
MemoryAccounting=yes

Then assign your service to this slice:

[Service]
Slice=memory-limited.slice

To make your application properly handle memory limits:

// C/C++ example
void* safe_malloc(size_t size) {
    void* ptr = malloc(size);
    if (!ptr) {
        syslog(LOG_ERR, "Memory allocation failed");
        // Handle error or exit gracefully
    }
    return ptr;
}