Build an Uptime Monitor with curl, poller, jobs, cron, and email

Introduction

This post builds a self-hosted uptime monitor and bottles it into a single command: aux4 monitor <url>. You'll make an HTTP request with curl, retry it with poller, run the check as a background job with jobs that emails an alert through email on failure, package that into your own monitor command, and schedule it with cron. The result probes a site every few minutes and emails you the moment it stops responding — no monitoring SaaS required.

Install the packages

aux4 aux4 pkger install aux4/curl aux4/poller aux4/jobs aux4/cron aux4/email

Step 1: make the request

aux4 curl request is a pipe-friendly HTTP client. By default it prints only the response body:

aux4 curl request https://example.com
<!doctype html><html lang="en"><head><title>Example Domain</title>...</body></html>

Add --showHeaders true to see the status line and headers:

aux4 curl request --showHeaders true https://example.com | head -1
HTTP/2.0 200 OK

That first line is your signal: 200 means healthy, anything else (or a connection error) means trouble.

Step 2: poll until healthy (or give up)

A single request tells you the state right now, but a blip isn't an outage. aux4 poll runs a command on an interval until the output matches a success pattern, hits a failure pattern, or times out:

aux4 poll \
  --command="aux4 curl request --showHeaders true https://example.com | head -1" \
  --expectation="*200*" \
  --maxWait=20 --interval=5
[0 s] HTTP/2.0 200 OK
Complete

The flags:

  • --command — the command to run each cycle.
  • --expectation — the success pattern, matched with shell glob rules: *200* matches output containing 200. Use a pattern with no spaces.
  • --maxWait — total seconds to keep trying (default 900).
  • --interval — seconds between attempts (default 30).

Each cycle prints [<elapsed> s] <output>. On success it prints Complete and exits 0; on timeout it prints Timeout and exits 1:

[0 s] HTTP/2.0 503 Service Unavailable
[5 s] HTTP/2.0 503 Service Unavailable
Timeout

Because poll keeps retrying until maxWait, a single blip never alerts — only a sustained outage does. --interval and --maxWait set how many attempts to allow first: --interval=20 --maxWait=60 checks at 0s, 20s, and 40s — three strikes before the check fails.

That non-zero exit is what makes it composable. You can also fail fast on a known-bad response with --failValue:

aux4 poll \
  --command="aux4 curl request --showHeaders true https://example.com | head -1" \
  --expectation="*200*" \
  --failValue="*503*" \
  --maxWait=120 --interval=15

Step 3: alert by email

aux4 email send sends mail over SMTP. Credentials live in a YAML config file — by default ~/.config/aux4/email.yaml — so no password ends up on the command line:

imap:
  host: imap.gmail.com
  port: "993"
smtp:
  host: smtp.gmail.com
  port: "465"
user: alerts@yourdomain.com
password: xxxx xxxx xxxx xxxx

For Gmail, Outlook, or Yahoo, use an app-specific password. The sender address is the user: field — there is no --from flag.

Sending is a one-liner:

aux4 email send \
  --to ops@yourdomain.com \
  --subject "ALERT: example.com health check failed" \
  --body "The health endpoint did not return 200 in time."

On success it prints the message ID:

Email sent successfully. Message ID: <abc123@smtp.gmail.com>

Step 4: alert on failure with a job hook

poll exits non-zero when the check fails. Hand the whole check to aux4 jobs run: it runs the command in the background and fires a hook based on the exit code. --onFailure sends the email only when the poll fails — no shell || glue, and every run is recorded:

aux4 jobs run \
  --command "aux4 poll --command='aux4 curl request --showHeaders true https://example.com | head -1' --expectation='*200*' --maxWait=20 --interval=5" \
  --onFailure "aux4 email send --to ops@yourdomain.com --subject 'ALERT: example.com is down' --body 'Health check failed.'"

It returns the job immediately:

{
  "id": "1",
  "state": "RUNNING",
  "exitCode": 0,
  "onFailure": "aux4 email send ..."
}

jobs list shows how it ended — COMPLETED when the site is up (no email), FAILED when it isn't (the hook fired the alert):

aux4 jobs list
[
  {
    "id": "1",
    "state": "COMPLETED",
    "exitCode": 0,
    "command": "aux4 poll ..."
  }
]

Step 5: bottle it into one command

That one-liner does the job, but you don't want to retype it. Wrap it in an .aux4 file so it becomes a command with the URL as an argument. Create .aux4:

{
  "profiles": [
    {
      "name": "main",
      "commands": [
        {
          "name": "monitor",
          "execute": [
            "aux4 jobs run --command \"aux4 poll --command='aux4 curl request --showHeaders true ${url} | head -1' --expectation='*200*' --maxWait=${maxWait} --interval=${interval}\" --onFailure \"aux4 email send --to ${email} --subject 'ALERT: ${url} is down' --body 'Health check failed.'\""
          ],
          "help": {
            "text": "Monitor a URL and email an alert if it stops responding",
            "variables": [
              { "name": "url", "text": "URL to monitor", "arg": true },
              { "name": "email", "text": "Alert recipient", "default": "ops@yourdomain.com" },
              { "name": "interval", "text": "Seconds between retries", "default": "10" },
              { "name": "maxWait", "text": "Max seconds to wait for a healthy response", "default": "60" }
            ]
          }
        }
      ]
    }
  ]
}

aux4 picks up the .aux4 in your working directory, so the command is ready:

aux4 monitor https://example.com
{
  "id": "1",
  "state": "RUNNING",
  "command": "aux4 poll ...",
  "onFailure": "aux4 email send ..."
}

The URL is a positional argument; --email, --interval, and --maxWait have defaults you can override. Install it as a package to run aux4 monitor from any directory.

Step 6: run it on a schedule

With the whole check behind one command, scheduling it is trivial — point cron at aux4 monitor:

aux4 cron start
aux4 cron add --name uptime-check --every "5 min" --run "aux4 monitor https://example.com"
{
  "name": "uptime-check",
  "every": "5 min",
  "run": "aux4 monitor https://example.com",
  "state": "active"
}

Each firing is recorded in the history with its job id:

aux4 cron history --name uptime-check
[
  {
    "name": "uptime-check",
    "jobId": "1",
    "timestamp": "2025-11-19T14:00:00Z",
    "status": "TRIGGERED"
  }
]

Pause, resume, or remove it without restarting the daemon:

aux4 cron pause --name uptime-check
aux4 cron resume --name uptime-check
aux4 cron remove --name uptime-check

Conclusion

You built aux4 monitor <url> out of five single-purpose packages:

  • curl makes the request,
  • poller turns a one-shot check into a patient retry loop with a clean exit code,
  • jobs runs that check in the background and fires the alert on failure,
  • email delivers the alert,
  • cron runs the command on a schedule and keeps a history.

The composition lives in one small .aux4 file, so aux4 monitor https://example.com is all anyone has to remember. Swap the URL for an internal service, point the expectation at a JSON field, or schedule a dozen of them — the command stays the same.

See Also