I run a one-person business. That's not a humble-brag — it's a warning label. It means every process that stops is a process I personally have to notice, walk over to, and restart. There's no on-call rotation. There's just me, usually at 11:30 p.m., realizing something has been quietly dead since lunch.
So about sixty days ago I made a decision. One of my AI agents — the one that triages my support inbox, tags each message, and drafts a first-pass reply for me to approve — was going to move off my laptop and onto a Raspberry Pi 4 sitting on a shelf. And it was going to run there, unattended, for as long as I could make it.
Sixty days later it's still running. I have not SSH'd in to restart it once. I want to be upfront about what this article is, because the answer turned out to be almost insultingly mundane. There's no clever framework, no exotic scheduler, no custom orchestration layer. The whole trick is one systemd unit file plus two small habits. But getting to "boring and reliable" cost me three distinct failures, and I think the failures are the useful part.
Failure #1: I ran it in tmux and called it a day
The first version of "deployment" was what everyone does first: I opened a tmux session, ran python agent.py, detached, and felt very clever.
It lasted four days. Then there was a brief power blip in my building — the kind too small to trip anything dramatic, just enough to reboot a Pi with no UPS. When power came back, the Pi booted fine, and my agent did not. tmux does not resurrect itself after a reboot. The process simply wasn't there anymore.
I found out because my support inbox quietly piled up for six hours before I happened to open it. Nothing errored. Nothing alerted. The absence of a process makes no noise. That's the thing nobody tells you about running agents: the failure mode isn't a crash you can see, it's a silence you have to notice.
Lesson: if your automation doesn't come back on its own after a power cut, it isn't automation. It's a process you're babysitting with extra steps.
Failure #2: the agent logged itself into a full disk
Once I moved it to systemd with Restart=always, I thought I was done. The agent ran fine for about two weeks. Then it started crash-looping.
The Pi's SD card was 100% full. My agent, it turned out, was logging far more than I'd given it credit for — it dumped the full text of every email it read, plus the full draft reply, at INFO level, every few minutes. Weeks of that filled the card. And because the disk was full, the agent couldn't write its own logs, which raised an exception, which Restart=always dutifully restarted, which failed again. A perfect little loop of self-inflicted denial.
I fixed it two ways. First I turned the logging down — the agent now logs a one-line summary per message, not the payload. Second, and more important, I stopped trusting the SD card to hold unlimited history. I pointed the agent's output at journald and capped journald's disk usage, so the system itself enforces "logs are allowed to exist, but not forever."
Lesson: an agent that runs forever will eventually do every thing it's capable of doing to you, and running out of disk is on that list. Assume it will try.
Failure #3: the one I didn't see coming — silent OOM
This was the sneaky one. After the disk fix, the agent was stable for a while, but I noticed that every couple of days systemctl status showed a recent restart I hadn't triggered. The agent came back up (good — that's what Restart=always is for), but I didn't know why it was dying.
dmesg told me. The kernel's OOM killer was shooting the agent. It was a memory thing: the agent keeps a rolling window of recent context in RAM so its replies stay coherent, and I had a bug where that window was never trimmed. On busy days, memory crept up until the kernel stepped in.
Two fixes. I patched the trim bug (embarrassing, one line). And I added a hard ceiling with MemoryMax=512M in the unit file, so if the agent ever starts leaking again, it gets killed and restarted at a predictable point instead of dragging down everything else on the Pi. A controlled crash you can reason about beats a mysterious one you can't.
Lesson: "it restarted itself" is not the same as "it's healthy." If you don't know why your agent restarts, you don't have reliability — you have a coin flip that's been landing heads for a while.
The boring config that actually fixed it
Here's the whole thing. This is the unit file at /etc/systemd/system/triage-agent.service:
[Unit]
Description=Support inbox triage agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=agent
WorkingDirectory=/home/agent/triage
ExecStart=/home/agent/triage/.venv/bin/python agent.py
Restart=always
RestartSec=10
MemoryMax=512M
StandardOutput=journal
StandardError=journal
SyslogIdentifier=triage-agent
[Install]
WantedBy=multi-user.target
Three lines do almost all the work:
-
Restart=always+RestartSec=10— if the process dies for any reason (crash, OOM, reboot), systemd brings it back ten seconds later. This is the entire answer to Failure #1. -
MemoryMax=512M— a hard ceiling that turns a slow leak into a predictable, survivable restart instead of a mystery. Answer to Failure #3. -
StandardOutput=journal— logs go to journald, which I cap, instead of a file that can eat the SD card. Answer to Failure #2.
Enable it once with sudo systemctl enable --now triage-agent and it survives reboots forever.
The one extra habit is a heartbeat. My agent touches a file every time it finishes a loop:
from pathlib import Path
import time
def beat():
Path("/run/triage-agent/heartbeat").write_text(str(time.time()))
And a separate systemd timer runs every five minutes to check that file's age. If the heartbeat is stale — meaning the agent is alive but stuck, which is the worst kind of failure — it restarts the service. This catches the one thing Restart=always can't: a process that's technically running but doing nothing.
What I'd tell someone starting today
If you're about to put an agent on a Pi (or anywhere) and want it to survive contact with the real world:
-
Never run it in a terminal session. If it doesn't have a systemd unit with
Restart=always, it's not deployed — it's just running. -
Cap its memory. Pick a number, set
MemoryMax, and accept that a controlled restart is a feature. -
Don't let logs grow without bound. Use journald with a size cap, or logrotate with
copytruncate. A full disk is the most preventable outage and also one of the most common. - Add a heartbeat, not just a process check. A running process can still be a stuck process. Check that work is actually happening.
-
Read
dmesgonce a week for the first month. Your agent will try to tell you things. The kernel is where it says them.
None of this is interesting. That's the point. Sixty days of uptime didn't come from a clever tool. It came from deciding that the agent had to restart itself, bound its own memory, and prove it was doing work — and then letting a very dull config file enforce all three.
The agent still triages my inbox every morning. I mostly just approve drafts now. And the shelf it lives on has quietly become the most reliable employee I have.
I write up the specific playbooks in The Solo Operator's AI Agent Playbook — code LAUNCH90 at checkout makes it $1.90. If it doesn't save you 5 hours in week one, reply to the receipt for a refund. Enter the code in the discount field on the checkout page itself (a ?discount= link won't apply it).
Top comments (0)