Keeping Tiny Services Alive Without an On-Call Rotation
Running a few small web services has forced me to rethink monitoring. My mental model of “being responsible for servers” was shaped by a 24/7 on-call rotation at a past job—dashboards, pager duty, and 2am wake-ups. That made me wary of running anything but static sites for a long time.
The reality is that my current projects—an nginx playground, a DNS tool, and a lookup utility—are low stakes. If one goes down for an afternoon, nobody is hurt. The goal isn't five-nines reliability; it's to spend almost zero time on operations while keeping the sites mostly working.
The Problem With No Monitoring
For a while, I ran no monitoring at all. The predictable outcome: sites broke and I only found out when someone told me. That was the nudge I needed to set up something minimal, but effective.
My approach uses three layers: an external uptime checker, an application-level healthcheck, and an automatic restart on failure. It works well for small, low-stakes systems, and it's not meant for mission-critical infrastructure.
External Uptime Checks
The first layer is a hosted uptime checker. I've been using updown.io and uptime robot. updown's pricing (per request) and UI suit me better, but uptime robot’s free tier is more generous.
These services do two things:
- Verify the site responds
- Email me when it doesn't
Email notifications hit the right balance—I find out quickly if something's wrong, but I won't be paged in the middle of the night. No one is going to wake up for a hobby website.
End-to-End Healthchecks
A simple “is the process up” check isn't enough. I initially wrote a healthcheck handler that returned 200 OK unconditionally. It confirmed the server was running, but it often reported success when the actual API was in a bad state. The service would be up, but not functional.
The fix was to make the healthcheck perform a real request through the service's core logic. My services are small—some have only a single endpoint—so this is easy. For the nginx playground, the handler makes another POST request to itself and reports success only if that request succeeds.
func healthHandler(w http.ResponseWriter, r *http.Request) {
// make a request to localhost:8080 with `healthcheckJSON` as the body
// if it works, return 200
// if it doesn't, return 500
client := http.Client{}
resp, err := client.Post("http://localhost:8080/", "application/json", strings.NewReader(healthcheckJSON))
if err != nil {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
if resp.StatusCode != http.StatusOK {
log.Println(resp.StatusCode)
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
Frequency: Hourly is Fine
Most of my healthchecks run hourly, with a few at 30-minute intervals. I monitor 18 different URLs on updown.io, where pricing scales per check. At $5/year for the budget, hourly checks keep costs minimal.
An hour of downtime before I'm alerted is acceptable. There's no guarantee I'll fix a problem immediately anyway. If checks were free, I'd run them every 5–10 minutes, but there's little practical need at this scale.
Restart on Failure
Services hosted on fly.io can leverage its built-in healthcheck-and-restart feature. I configure an HTTP healthcheck that, on failure, triggers a service restart.
Restarting aggressively is a useful stopgap for unpatched bugs. The nginx playground once had a process leak—nginx processes weren't being terminated—which eventually exhausted RAM. The failure cycle looked like this:
- Server runs out of RAM
- Healthcheck starts failing
- Service gets restarted
- Everything works again
- Repeat hours later
It was a fine workaround while I procrastinated on the root cause. These safety-net checks run more often, around every 5 minutes.
This approach isn't suitable for complex, large-scale services—a single HTTP healthcheck wouldn't cover the required surface there. For my small workloads, it has proven sufficient.
I held this article back for three months to validate the setup. In that time, availability for the monitored sites was 99.95%, a major improvement over the ad-hoc, no-monitoring approach. The uptime checker and end-to-end healthchecks eliminated the silly, unnoticed outages I was having before. The system isn't perfect, but it achieves my goal: the sites mostly work, and I spend almost no time thinking about operations.



