HomeTopicsMonitoring & Alerting › Status Monitoring

Monitoring Job Status: Exit Codes, Logs, and Heartbeats

Nobody watches a transfer job run. That is the whole point of automation — and the whole problem of monitoring. Once a job runs unattended at three in the morning, the only way you will ever know how it went is through the evidence it leaves behind. That evidence comes in exactly three forms: the exit code the process returns, the log lines it writes, and the heartbeat it can be taught to emit. Everything a monitoring system does, from the simplest cron mail to the fanciest commercial platform, is built out of these three signals.

Each signal proves something different, and — this is the part that separates working monitoring from decorative monitoring — each one has a blind spot the others must cover. This article takes the three in turn: what each actually proves, the specific ways each one lies, and how to combine them into a small, honest status system for a modest estate using nothing but scripts, a scheduler, and email. It is part of our Monitoring & Alerting series, sitting between why jobs fail silently (the problem) and alert design (what to do when a signal goes bad).

Three Signals, Three Different Proofs

Before the details, fix the map in your head. The diagram below shows one scheduled transfer job and the three channels of evidence it can produce: the exit code reported to the scheduler, the log file written during the run, and the heartbeat file stamped at the end of a successful run — with what each channel can and cannot tell you.

Transfer job runs unattended 1. Exit code seen by the scheduler 2. Log lines written during the run 3. Heartbeat stamped after success proves: it ended, and how blind: says nothing if the job never started proves: what happened inside blind: brittle to reword, rotation, and absence proves: a run completed lately blind: needs a separate checker to notice staleness No single signal is sufficient — the design question is how they cover each other.

Signal One: Exit Codes — the Scheduler's View

An exit code is the single small number every program hands back to whatever started it when it finishes: zero means "I claim success," anything else means "I am reporting failure." It is the oldest status mechanism in computing and still the most load-bearing, because it is the one signal your scheduler natively understands. Windows Task Scheduler records it as the task's Last Run Result; cron does not inspect it directly, but a script can, and cron's habit of mailing any output a job produces gives failures a route out — the mechanics live in our bash and cron series and the scheduled jobs series.

What an exit code proves is precise and narrow: the process ran to completion and assessed itself. Its value depends entirely on how honest that self-assessment is, which puts two obligations on you as the script author:

  • Propagate failure upward. The classic sin is a wrapper script that calls the transfer client, ignores the client's exit code, and then exits zero because its own last command (an echo, a cleanup) succeeded. The scheduler sees success; the transfer failed. Every layer — client, script, scheduler — must pass the verdict up unmodified.
  • Exit non-zero for hollow success. A run that completed but moved zero files when files were expected should exit non-zero, as covered in why jobs fail silently. The exit code is your chance to encode the business verdict, not just the mechanical one — use distinct codes for distinct failure classes if your scheduler or checker can act on them.

Then make the code reach a human. Checking Last Run Result by hand every morning does not scale past about three jobs; the workable pattern is to have the outcome generate a message automatically. If your jobs run through an automation tool, this usually comes built in — Sysax FTP Automation, for example, can send an email notification when a task succeeds or fails, and because its tasks carry their own retry and error handling, a failure notification means the retries are already spent and a human is genuinely needed. That combination — honest verdict, automatic delivery — is the entire first layer of status monitoring.

The blind spots: an exit code exists only if the job ran, so it is structurally silent about the job that never started; and it compresses the whole run into one number, so it can tell you that the job failed but rarely why. The first blind spot belongs to heartbeats; the second belongs to logs.

Signal Two: Logs — the Detail, Honestly Handled

The log is the job's diary: connection attempts, files transferred, byte counts, errors with their context. When an exit code says "failed," the log says "failed because the remote host refused the connection at the third retry." For diagnosis, logs are irreplaceable, and our logging series covers writing them well — what to log — and reading them under pressure — reading transfer logs.

The tempting next step is to make logs a monitoring signal: a scheduled check that scans the log for ERROR and alerts on a hit. This is log scraping, and it works — with fragilities you must respect, because each one fails silently, and a silent failure in your monitoring is the worst place to have one:

  • Format drift. Your matcher looks for Transfer failed; an update or an edit rewords the message to Unable to complete transfer. The scrape now matches nothing, forever, and "no matches" is indistinguishable from "no errors." Any change to the software that writes the log can quietly disarm the check.
  • Rotation races. Logs get rotated — renamed and restarted — so they do not grow without bound. A checker that opens job.log just after rotation reads a nearly empty file and finds no errors; one that holds the file open across rotation keeps reading the old file while new lines land in the new one. Windows adds a twist: a checker holding the log open can block the rotation itself. Every log-scraping scheme needs an answer for the moment of rollover.
  • The absence trap. "No ERROR lines since midnight" is consistent with a perfect run — and equally consistent with a job that never ran and wrote nothing at all. Scraping for badness cannot distinguish silence from health.
  • Parsing hazards. Multi-line error dumps, timestamps in local formats, messages containing the very keywords you match on ("checking for ERROR conditions: none") — free text is a hostile parsing target.

The honest conclusions: scrape for the presence of success, not merely the absence of failure — have each job write one final, machine-first verdict line, such as RESULT: OK files=12 bytes=48211, and let the checker require today's RESULT: OK rather than hunt for scary words. Treat a missing or stale log as a failure in its own right. And keep the matcher's contract written down, so whoever rewords a log message knows there is a dependent. If your logs are centralized, run the checks in one place against everything — the architecture is in centralizing logs.

Remember: a log check that searches only for error text reports "all clear" in three very different worlds — the job succeeded, the job never wrote a log, or the error message changed shape. Only a positive check ("today's OK line exists") collapses those three into one honest answer.

There is also a structural way around log parsing for the server side of your flows: query, don't scrape. A server that records activity to a database as well as a file gives you rows and columns instead of free text — Sysax Multi Server logs all activity to both, with automatic rollover, so a status question like "how many files did the partner account upload since midnight?" becomes a small query with a numeric answer rather than a pattern match with failure modes.

Signal Three: Heartbeats — Proof of Life

The third signal fixes the blind spot the other two share: neither can report on a job that never ran. A heartbeat is a tiny file (or database row) the job rewrites at the end of every successful run — a timestamp plus a one-line verdict. Its meaning is deliberately minimal: a run completed at this time. Freshness of the heartbeat, not its content, is the signal.

# last line of the job, only reached on success
echo "OK $(date "+%a %b %d %H:%M") files=$count" > /var/status/invoices_pull.hb

Alone, a heartbeat file proves nothing — the point is the checker that watches it: a separate scheduled script that alerts when any heartbeat is older than its job's schedule allows. A nightly job's heartbeat should never be more than about twenty-six hours old; an hourly job's, never more than about ninety minutes. The checker is a dozen lines:

# heartbeat checker — runs every 15 minutes on a different host if possible
for each entry in heartbeats.conf:        # name | file | max_age_minutes
    if file is missing or older than max_age:
        alert "job [name] has not completed since [file timestamp]"

This inversion has a name worth knowing: the dead-man's switch, after the handle a train driver must hold — release it, and the brakes apply on their own. Normal alerting fires when a bad signal arrives; a dead-man's switch fires when a good signal stops arriving. The job does not have to detect anything, announce anything, or even exist anymore: if it stops completing, the heartbeat goes stale and the alarm raises itself. That is why this pattern — also called alert-on-no-data — is the only one of the three signals that catches a disabled schedule, a powered-off host, a locked-out service account, or a deleted task. The same principle applied to the files themselves, rather than the jobs, is the freshness check: heartbeats watch jobs, freshness checks watch outcomes, and mature estates run both.

Design notes that keep heartbeats trustworthy: write the heartbeat only on genuine success — after the outcome assertion, never in a cleanup path that runs regardless; keep all heartbeats in one status directory so the checker (and tomorrow's status report) has one place to look; and put the checker somewhere that does not share fate with the jobs — a checker on the same host as the jobs sleeps through the host's own death.

What Each Signal Catches: The Coverage Table

Put the three side by side against the failure modes that actually occur, and the design writes itself:

Failure mode Exit code Log check Heartbeat
Job never ran (schedule off, host down) Silent — no code exists Silent, unless checking for missing OK line Caught — heartbeat goes stale
Job crashed mid-run Caught, if the scheduler's result is watched Partial — log ends abruptly, no verdict line Caught — no fresh heartbeat written
Job failed and said so Caught — non-zero code Caught — error plus missing OK line Caught — heartbeat stale by next window
Ran "successfully" but moved nothing Caught only with an outcome assertion in the job Caught only if the verdict line carries counts Missed — a hollow run still beats the heart
Expected file never arrived from a sender Silent — no job of yours failed Silent Silent — needs a freshness check instead

Read the columns and the strategy is plain: exit codes are the cheap, immediate failure signal; heartbeats are the only cure for "never ran"; logs carry the diagnosis and the counts; and the last two rows are why freshness checks exist as a fourth, outcome-facing layer. None of the signals is optional because none is complete.

Wiring It Up for a Small Estate

You do not need to buy anything to monitor a dozen transfer jobs well. You need conventions, one checker, and email. The whole design fits in five lines:

  1. Every job ends with a verdict and a heartbeat. One machine-first RESULT: line to its log, one heartbeat file to the shared status directory, exit code to match. Failures skip the heartbeat and exit non-zero. This is a ten-line change per job, and patterns for it live in our bash and scheduler series.
  2. Failures notify immediately. Scheduler-triggered mail, a mail step in the script's error path, or the automation tool's built-in success/failure notifications — whichever fits, routed to a shared mailbox, with retries handled before the noise starts (see the retry and error handling series).
  3. One checker watches the heartbeats. Every fifteen minutes, staleness check across the status directory; alerts name the job and the last known good run.
  4. Freshness checks watch the critical outcomes. Per-flow deadlines for the files the business would phone you about.
  5. Everything also lands in a morning report. The same status directory that feeds the checker feeds a one-page daily summary — built in the visibility article — so quiet weeks stay visible too.

That system is two short scripts and a discipline, and it will carry an estate of a few dozen jobs for years. The signs you have outgrown it are organizational, not technical: multiple teams needing to see status, an on-call rotation that needs acknowledgement and escalation rather than a mailbox, hundreds of jobs across many hosts. At that point a dedicated monitoring system earns its keep — and everything transfers, because commercial and open monitoring tools are built from exactly these primitives: your heartbeat checker becomes their alert-on-no-data rule, your verdict lines become their check results, your max-age values become their thresholds. Nothing you built was wasted; it was the specification.

Start here: if you do exactly one thing after reading this article, add the RESULT: line and heartbeat write to your three most important jobs and schedule the fifteen-line staleness checker. That single afternoon closes the "job never ran" hole — the one failure mode that today has no signal at all.

The Signals, Braided

Exit codes tell the scheduler how a run judged itself; logs record what actually happened inside; heartbeats prove that runs keep completing at all. Each lies in a characteristic way — hollow zeros, drifting formats, hearts that beat through empty runs — and the craft of status monitoring is arranging them so every lie is caught by a neighboring signal.

From here, two directions: upward to alerts people actually read, because a perfect signal wasted on an unread message is still a silent failure; and sideways to monitoring the monitoring, because the checker you just built is itself a job that can die quietly — and it deserves a dead-man's switch of its own.

Frequently Asked Questions

What exactly is an exit code?
It is the number every program returns to its parent when it finishes: zero for success, non-zero for failure. Schedulers record it — Windows Task Scheduler as the Last Run Result, and shell scripts read it as $? or $LASTEXITCODE. It is only as truthful as the script that produces it, which is why wrappers must pass the transfer client's code through instead of exiting zero unconditionally.
Why isn't searching the log for "ERROR" good enough?
Because "no matches" has three meanings: the run was clean, the job never ran and wrote nothing, or the error message was reworded and your pattern no longer matches. Rotation can also swap the file out from under the checker. Search instead for the presence of today's success line — a positive check fails loudly when anything in the chain breaks.
What is a dead-man's switch in monitoring?
An alarm triggered by the absence of a good signal rather than the arrival of a bad one — named after the train handle that applies the brakes if the driver lets go. In job monitoring: each job refreshes a heartbeat on success, and a separate checker alerts when a heartbeat goes stale. It is the only pattern that catches a job that never started.
What is the difference between a heartbeat and a freshness check?
A heartbeat watches a job: "a run of this task completed recently." A freshness check watches an outcome: "the file the business expects is actually here and recent." A hollow run beats the heart without producing the file, and a sender-side failure stops the file without touching your jobs — so the two checks cover different failures and work best together.
Do I need a monitoring product for this?
Not to start. Verdict lines, heartbeat files, one staleness checker, and email will monitor a small estate honestly. Consider a dedicated monitoring system when scale or on-call process demands it — dozens of hosts, acknowledgement and escalation, shared dashboards. The conventions you build now map directly onto whatever tool you adopt later.
What should a heartbeat file contain?
The freshness of the file is the real signal, but one line of content makes it far more useful: a timestamp, the verdict, and headline numbers — for example OK Mar 14 02:31 files=12 bytes=48211. The checker uses the file's age; the daily report and a human at a console use the line.

From the Sysax team: we build secure file transfer software for Windows — Sysax Multi Server, an FTP, FTPS, SFTP, and HTTPS server, and Sysax FTP Automation for scheduled, scripted transfers. Free trials are on the download page.