AI-generated systemd services often pass a smoke test because the unit is syntactically valid, but they fail later because the model forgot resource controls. The core problem is that a minimal generated unit typically declares only ExecStart, User, and Restart, so the service can consume an entire free server's CPU or memory. This article presents a four-step workflow that turns four systemd directives into a measurable ceiling contract and probes the service before enabling it.
The workflow is practical when you generate the service with MonkeyCode's free model access and run it on MonkeyCode's free server option, although every command also works on any Linux host with systemd. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Why the default is the failure mode
The generated unit below is valid but unsafe on a small host. It starts a Python worker as a dedicated user, restarts it on failure, and binds only to loopback.
[Unit]
Description=AI worker
After=network.target
[Service]
Type=simple
User=worker
ExecStart=/opt/worker/server.py
Restart=on-failure
The absence of limits is not a syntax error, so review tools such as systemd-analyze verify report no problem. That is exactly why the generated unit can pass a review gate and still cause a later outage. A missing ceiling is an operational bug that only appears when the worker receives enough parallel requests to exhaust the host.
What the four ceilings measure
Each directive blocks a different resource failure mode. The table below summarizes the four fields used in the contract.
| Directive | What it stops | Risk if omitted |
|---|---|---|
MemoryMax |
A worker that allocates until the host OOM-kills another process | Hard memory exhaustion |
MemoryHigh |
A worker that grows steadily but has not hit the hard cap yet | Sudden pressure on swap and page cache |
CPUQuota |
A CPU-bound loop that occupies every available core | Other services on the free server become unresponsive |
TasksMax |
A thread pool or fork loop that creates hundreds of processes | PID exhaustion and scheduler overload |
LimitNOFILE |
A connection leak that exhausts file descriptors | New sockets or files fail unpredictably |
Add these directives as a drop-in override rather than editing the generated unit directly. The generated unit remains easy to regenerate when the model output changes, while your policy stays separate and reviewable.
# /etc/systemd/system/ai-worker.service.d/10-resource-limits.conf
[Service]
MemoryHigh=192M
MemoryMax=256M
CPUQuota=60%
TasksMax=32
LimitNOFILE=128
Reload systemd and start the service.
sudo systemctl daemon-reload
sudo systemctl start ai-worker.service
Step 1: Add a ceiling from host headroom
Choose values from the host's expected headroom, not from the model's confidence. On a small free server with limited shared resources, a hard memory cap such as 256M may be a reasonable starting point when the service is a single Python worker, but the number must come from repeated measurement on the same host class. Keep a reserve for sshd, systemd itself, and the package manager, because the whole point is to prevent one service from taking that reserve.
Step 2: Run the generated service against a controlled probe
The worker below is intentionally small so you can reproduce the test without a framework. It exposes a /load route that burns CPU for approximately half a second per request.
#!/usr/bin/env python3
from http.server import BaseHTTPRequestHandler, HTTPServer
import time
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/load":
end = time.time() + 0.5
n = 0
while time.time() < end:
n = (n + 1) % 1_000_000
body = b"ok\n"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(204)
self.end_headers()
HTTPServer(("127.0.0.1", 8000), Handler).serve_forever()
Save the probe script beside the service definition so it can run on any fresh host before merge. The script starts the unit, sends 32 parallel requests, and then reports CPU seconds consumed and the current resource state.
#!/usr/bin/env bash
set -euo pipefail
UNIT="${1:-ai-worker.service}"
PORT="${2:-8000}"
systemctl is-active --quiet "$UNIT" || systemctl start "$UNIT"
sleep 2
cpu_before=$(systemctl show -p CPUUsageNSec --value "$UNIT")
for i in $(seq 1 32); do
curl -sSf "http://127.0.0.1:${PORT}/load" >/dev/null &
done
wait
sleep 3
cpu_after=$(systemctl show -p CPUUsageNSec --value "$UNIT")
awk -v before="$cpu_before" -v after="$cpu_after" \
'BEGIN { printf "cpu_seconds_used=%.3f\n", (after - before) / 1000000000 }'
systemctl show "$UNIT" \
--property=MemoryCurrent,CPUUsageNSec,TasksCurrent,MemoryMax,CPUQuotaPerSecUSec,TasksMax,LimitNOFILE
A representative report might look like the output below. The exact numbers will differ across hosts and request counts.
cpu_seconds_used=2.940
MemoryCurrent=83886080
CPUUsageNSec=2940000000
TasksCurrent=29
MemoryMax=268435456
CPUQuotaPerSecUSec=600000
TasksMax=32
LimitNOFILE=128
Step 3: Compare observed behavior with the declared ceiling
Interpret the report as a simple pass or fail against the contract, not as a benchmark. CPUQuotaPerSecUSec=600000 means the service is allowed 0.6 CPU seconds for each wall second, so a five-second burst may use at most 3.0 CPU seconds. The observed value of 2.94 CPU seconds is close to that ceiling, which tells you the CPU limit is actively throttling under burst load. If the observed CPU seconds stayed near zero while requests timed out, the service would be I/O bound or the probe would be too weak. If TasksCurrent approached TasksMax, the worker would be near its thread cap and might need a larger limit or a smaller pool.
Memory is less useful as a single sample because it captures one moment after a short burst. Run the probe several times, record the peak MemoryCurrent, and then set MemoryMax above that peak with a 20 percent headroom if the host can afford it. Do not raise a limit merely because the service survived one run; repeated measurement is what makes the contract reliable.
Step 4: Gate the unit before merge
A final gate catches a regenerated unit that accidentally loses one of the resource directives. The check is deliberately simple and runs on any host with systemd.
#!/usr/bin/env bash
set -euo pipefail
UNIT="${1:-ai-worker.service}"
test "$(systemctl show -p MemoryMax --value "$UNIT")" != "infinity" || {
echo "MemoryMax is missing" >&2; exit 1; }
test "$(systemctl show -p CPUQuotaPerSecUSec --value "$UNIT")" != "infinity" || {
echo "CPUQuota is missing" >&2; exit 1; }
test "$(systemctl show -p TasksMax --value "$UNIT")" -lt 256 || {
echo "TasksMax is too high for this host" >&2; exit 1; }
echo "resource contract ok"
Run the gate in the same automation path that reviews AI-generated changes. It does not replace human review, but it makes the resource policy explicit and repeatable.
Limitations and when to use a different control
A systemd resource ceiling is not a sandbox. It prevents one class of exhaustion but does not isolate file writes, network destinations, or kernel attack surface. Pair this workflow with ProtectSystem=strict, PrivateTmp=yes, and NoNewPrivileges=yes when the generated code is untrusted. CPUQuota also does not throttle disk I/O or network packets, so add IOWeight or an external scheduler if those are the actual bottleneck. The approach is not sufficient for multi-tenant isolation or workloads that need exact latency guarantees; use a container runtime or VM when a stronger boundary is required. If you generate services with a free model, keep this probe script next to the generated unit so every new service gets a ceiling before it gets a start.
Top comments (0)