How to Set Resource Limits for Services Using systemd Cgroups on Fedora

You can set resource limits for systemd services on Fedora by editing the service unit file directly or creating an override with `systemctl edit`, using specific Cgroup directives like `MemoryMax`, `CPUQuota`, and `IOWeight`.

When a background service starts eating everything

You deploy a new web application or database on Fedora. It runs fine for a week. Then a traffic spike hits or a query goes recursive. The service grabs every megabyte of RAM and hogs a full CPU core. Your desktop freezes, your other services time out, and the only way to recover is a hard reboot. You need a way to cage the process before it takes down the host.

How systemd budgets work under the hood

Fedora uses the cgroups v2 unified hierarchy by default. The kernel divides system resources into a single tree structure. systemd maps every service, socket, timer, and user session into its own branch of that tree. When you set a limit, you are not asking the process to be polite. You are handing the kernel a hard budget. The kernel enforces it at the scheduler and memory allocator level. The process never sees the missing resources. It just gets throttled or killed when it crosses the line.

This design removes the need for external watchdog scripts or cron jobs that kill runaway processes. The enforcement is built into the init system. You define the boundary once, and the kernel handles the rest. The cgroups v2 model also simplifies debugging. There is no longer a split between CPU/memory controllers and I/O controllers. Everything lives under a single system.slice namespace. You can inspect the exact state of any service by reading the sysfs tree or querying the systemd manager.

Run this in a backup VM first if you can. A botched limit can leave you unable to boot or manage the system remotely. Test the constraints under load before applying them to production.

Setting limits with a drop-in override

Never edit the original unit file shipped by a package. Those files live in /usr/lib/systemd/system/ and get overwritten on the next dnf upgrade. Always use a drop-in override. The override lives in /etc/systemd/system/<service>.service.d/ and takes precedence over the package defaults. This keeps your customizations safe across package updates.

Open the override editor for your target service. Replace httpd with the actual unit name you want to constrain.

sudo systemctl edit httpd
# Opens a temporary editor. Creates /etc/systemd/system/httpd.service.d/override.conf automatically.
# Leaves the original package file untouched.
# Exits and saves when you close the editor.

Paste the resource directives into the [Service] section. systemd reads these values at startup and applies them to the cgroup. The syntax accepts absolute values, percentages, and human-readable suffixes like M or G.

[Service]
# Hard memory ceiling. The kernel OOM killer triggers if the service exceeds this.
MemoryMax=512M
# CPU budget as a percentage of one core. 50% means half a core across all CPUs.
CPUQuota=50%
# I/O scheduling priority. Lower numbers get less disk time. Default is 100.
IOWeight=100
# Maximum open file descriptors. Replaces the legacy ulimit for this service.
LimitNOFILE=1024

Save the file and exit. The override is now on disk. systemd does not read new files until you tell it to. Reload the manager configuration and restart the service to apply the budget.

sudo systemctl daemon-reload
# Tells systemd to rescan /etc/systemd/ and pick up the new override file.
sudo systemctl restart httpd
# Restarts the process inside the newly configured cgroup.

Run systemctl status httpd before you restart. Check the active state and the recent log lines. If the service fails to start, the journal will show why. Do not guess. Read the actual error before adjusting the limits again.

Confirming the kernel is enforcing the budget

You need to verify that systemd actually applied the constraints. The systemctl show command queries the runtime manager and returns the active cgroup values. This confirms the configuration was parsed correctly.

systemctl show httpd --property=MemoryMax,CPUQuota,IOWeight
# Queries the running manager for the exact values applied to the unit.
# Returns the configured limits, not the current usage.
# Confirms the override file was parsed correctly.

To watch live resource consumption, use systemd-cgtop. It shows a real-time table of cgroup memory and CPU usage. This is faster than digging through process lists.

systemd-cgtop
# Displays a top-like view of cgroup resource consumption.
# Updates every second. Press q to exit.
# Shows memory usage and CPU percentage per slice or service.

If you prefer raw filesystem data, the cgroup v2 tree is mounted at /sys/fs/cgroup/. Every service gets a directory under system.slice/. You can read the exact kernel limits directly from the sysfs interface.

cat /sys/fs/cgroup/system.slice/httpd.service/memory.max
# Reads the kernel-enforced memory ceiling directly from the cgroup filesystem.
# Returns the value in bytes. 512M becomes 536870912.
# Confirms the kernel actually received the limit.

Check the live cgroup values before you assume the limits are working. The manager config and the kernel enforcement are two different steps. Trust the package manager. Manual file edits drift, snapshots stay.

What happens when the budget is too tight

Setting a limit that is too low will not gracefully degrade the service. It will crash it. The kernel OOM killer does not negotiate. When a process crosses MemoryMax, systemd sends a SIGKILL and logs the event. You will see a clear failure message in the journal.

systemd[1]: httpd.service: Main process exited, code=killed, status=9/KILL
systemd[1]: httpd.service: Failed with result 'oom-kill'.

If the service dies immediately after restart, check the journal for the unit. Use the -xe flags to get explanatory context and jump to the end of the log. Most sysadmins type journalctl -xeu <unit> muscle-memory style.

journalctl -xeu httpd
# Fetches logs for the specific unit.
# The x flag adds priority and explanatory notes.
# The e flag jumps to the most recent entries.

CPU throttling behaves differently. CPUQuota does not kill the process. It starves it of scheduler time. The service will appear hung or extremely slow. Network timeouts will pile up. If your application relies on background workers or database connections, a 50% quota might drop it below the minimum required throughput. Start with a higher quota and lower it gradually. Monitor response times before tightening the budget.

Disk I/O limits require a device path. IOReadBandwidthMax and IOWriteBandwidthMax expect a block device like /dev/sda or /dev/nvme0n1. If you reference a partition that does not exist or a device node that systemd cannot resolve, the unit will fail to start with a configuration error.

Failed to parse device path or bandwidth limit, ignoring: /dev/sda 10M

SELinux does not interfere with cgroup limits. The kernel handles resource accounting separately from mandatory access control. If a service cannot read its config files or bind to a port after you apply limits, the problem is permissions, not cgroups. Check journalctl -t setroubleshoot for denials before touching the resource budget. SELinux denials show up in the journal with a one-line summary. Read those before disabling SELinux.

Reboot before you debug. Half the time the symptom is gone.

Choosing the right constraint for your workload

Use MemoryMax when you need a hard ceiling that triggers the OOM killer if exceeded. Use MemoryHigh when you want the kernel to throttle the process instead of killing it, allowing graceful degradation. Use CPUQuota when you need to guarantee a fixed percentage of a single core across all available CPUs. Use CPUWeight when you want to adjust relative priority between competing services without fixing absolute time slices. Use IOWeight when you need to tune disk scheduling priority for services that share the same storage device. Use IOReadBandwidthMax and IOWriteBandwidthMax when you must cap absolute throughput in bytes per second to protect a shared disk from saturation. Stay on the default systemd limits if your service is stateless, short-lived, or runs in a container that already manages its own cgroups.

Where to go next