Cron and Task Scheduler Compared for Transfer Work
Most transfer estates are mixed. The Linux box in the corner pulls partner files with a cron job; the Windows machine next to it pushes reports through Task Scheduler; and the administrator responsible for both switches mental models a dozen times a day. The trouble starts when you assume the two schedulers think alike. They solve the same problem — run this program at that time — with philosophies so different that a habit carried from one to the other becomes a bug: the cron job whose output vanished because there was no mailer, the Windows task that broke on password rotation in a way no cron job ever could.
This article puts the two side by side, specifically for transfer work: how each defines a job, the environment each hands your script, where output goes, what happens to missed runs, how credentials and overlapping runs are handled, and what managing fifty jobs looks like on each. It closes with the conventions that keep a both-platforms shop sane. It is part of our Scheduled Jobs series; the deep Windows walkthrough lives in Task Scheduler for transfer jobs, and this piece assumes only that you have met both tools briefly.
Two Mental Models
Cron is a daemon — a background process — that reads schedule tables and runs commands when the clock matches. The table is called a crontab: a plain text file, one job per line, each line a time pattern plus a command. Each user can have one (edited with crontab -e), and system-wide tables exist too (/etc/crontab and drop-in files under /etc/cron.d, whose lines carry one extra field naming the user to run as). That is essentially the whole product. No job objects, no settings tabs, no history database. Cron's philosophy is minimalism: it starts your command at the right minute and considers its work done.
Task Scheduler is a Windows service managing a library of task objects. Each task is a structured bundle: one or more triggers, one or more actions, a security context (which account, logged on or not), conditions, and a settings page covering failures, missed starts, and instance rules. Tasks live in a hierarchy of folders, are edited in a console or with schtasks, and can be exported and imported as XML. Its philosophy is the opposite of cron's: model everything about the job, and have the scheduler manage lifecycle, not just launch.
A fair analogy: cron is a wall calendar with terse one-line entries; Task Scheduler is a filing cabinet with a labeled folder per job. The calendar is faster to read and easier to copy; the filing cabinet holds more of the story in the official record. Neither is wrong. But every difference in the rest of this article flows from that split.
Defining the Same Job on Each
Here is one transfer job — push the day's invoice files at ten past two every night — defined both ways:
# cron: crontab -e as the service user # minute hour day-of-month month day-of-week command 10 2 * * * /opt/jobs/invoice-push/push-invoices.sh >>/var/log/jobs/invoice-push.log 2>&1 # Windows: schtasks from an elevated prompt (or build it in the console) schtasks /create /tn "\Transfers\invoice-push-nightly" ^ /tr "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Jobs\InvoicePush\push-invoices.ps1" ^ /sc daily /st 02:10 /ru CORP\svc-invoicepush /rp *
The five cron time fields read minute, hour, day of month, month, day of week — 10 2 * * * is "02:10 every day." Two quirks are worth knowing before they bite. If you restrict both day-of-month and day-of-week, classic cron runs the job when either matches, not both — a surprise for "the first Monday" attempts. And a percent sign in the command field is special to cron (it becomes a newline), so it must be escaped as \% — the classic way a date-formatting command that works in the shell dies in the crontab. Notice also what the cron line must carry explicitly that Task Scheduler handles elsewhere: the output redirection into a log file, which on Windows would be pointless (more on that below), and the identity, which cron takes from whose crontab the line lives in.
The Environment Each Job Inherits
Here is the difference responsible for the most "works in my terminal, fails on schedule" tickets on both platforms.
Cron gives your job almost nothing. No login profile is sourced — .bashrc and .profile never run — the PATH is a sparse default like /usr/bin:/bin, the shell is /bin/sh rather than your interactive bash, and there is no terminal attached. A script that relies on a tool living in /usr/local/bin, or on an environment variable your profile exports, works for you and fails for cron. The fixes are discipline: absolute paths to programs, a PATH= line at the top of the crontab, and testing the script under a deliberately emptied environment before trusting the schedule:
# run the script roughly the way cron will: near-empty environment, sh, no terminal
env -i HOME=/home/svc-transfer SHELL=/bin/sh PATH=/usr/bin:/bin \
/bin/sh /opt/jobs/invoice-push/push-invoices.sh
Task Scheduler gives your job a different world, not an empty one. The task runs in a hidden, non-interactive session: no desktop, no mapped drive letters (use UNC paths), a system directory as the working directory unless the action's Start in field says otherwise, and — depending on settings — no fully loaded user profile. The failure shape is the same as cron's even though the mechanics differ: the interactive session quietly supplied something the scheduled run does not get.
The shared lesson is that the scheduler's world is not your terminal, and scripts must be written for the scheduler's world: absolute paths, explicit environment, no prompts. Our bash and cron automation series drills this for the Unix side, and the PowerShell transfer automation series for Windows.
Where Output Goes
Run a chatty transfer script from a terminal and you see its progress. Schedule it, and the two platforms do very different things with those words.
Cron mails it. Anything the job writes to standard output or standard error is collected and emailed to the crontab's owner, or to whatever address the MAILTO variable in the crontab names. On a machine with a working mail setup this is a crude but real notification channel — a silent job means success, mail means something spoke. On the many machines with no mailer configured, the output is simply lost, or piles up unread in a local spool file. The professional pattern is to take control: redirect output to a log file per job (as the crontab line above does), and reserve mail for genuinely unexpected noise.
Task Scheduler discards it. There is no console in the task's hidden session and no mail fallback; whatever the script prints evaporates. The task's history records that the process started and what exit code it returned — never what it said. On Windows, a scheduled script that does not write its own log has no story at all.
Either way you land on the same rule: a scheduled transfer job writes its own log, with timestamps, in a known place. What belongs in that log — filenames, counts, durations, outcomes — is the subject of what to log, and it applies identically on both platforms.
Missed Runs: The Honesty Section
What happens when the machine is off, asleep, or mid-patch at the scheduled minute? This is where assumptions imported from the other platform hurt most, so here it is plainly.
Classic cron skips the run, silently and permanently. Cron wakes each minute and runs what matches now; a minute that passed while the machine was down simply never existed as far as cron is concerned. No record, no catch-up, no error — the 02:10 job just did not happen, and nothing on the machine says so. Two mitigations exist in the cron world. The @reboot crontab keyword runs a command when the daemon starts, which lets a job compensate at boot. And anacron-style catch-up — a companion approach designed for machines that are not always on — tracks when daily, weekly, and monthly jobs last ran and runs any that are overdue shortly after the machine comes back. The honest limits: it works in day granularity, not minutes, so it suits "the nightly pull must eventually happen," not "at 02:10 sharp."
Task Scheduler also skips by default. The difference is that catching up is a per-task checkbox — "Run task as soon as possible after a scheduled start is missed" — which, when enabled, runs the missed task shortly after the machine returns (shortly, not instantly at boot). It also has startup triggers with configurable delays, the closer equivalent of @reboot with a snooze button.
Neither platform decides for you whether a late run is better than no run — that is a per-flow business decision, and getting it wrong either double-sends files or leaves a downstream system waiting on a file that will never come. The decision framework, plus making late runs safe, is the whole subject of jobs that survive reboots and missed windows.
Remember: both schedulers interpret times in local time, so the twice-yearly clock changes can skip or repeat an early-morning hour in regions that shift clocks — and the classic 02:00-to-03:00 window where transfer jobs love to live is exactly the hour affected. Schedule critical flows outside the shift window, or accept and document the twice-yearly oddity.
Identity and Credentials
A cron job's identity is implicit: it runs as the user whose crontab it lives in, full stop. Nothing is stored to make that happen — cron is already root and simply becomes the user. Whatever secrets the job needs to reach remote systems (an SSH key, a config file) live in the filesystem under that user's control.
A Task Scheduler job's identity is explicit and has a cost: the task names an account, and if it must run with nobody logged on, Windows stores that account's password so the scheduler can log it on. That stored copy is a small liability — local administrators can recover it — and a maintenance trap: rotate the account's password and every task holding the old one starts failing logon until updated. Cron simply has no equivalent failure mode, which is why "the password rotation broke the schedule" is a Windows-flavored incident.
On both platforms the right identity is a dedicated, least-privilege service account rather than a person's login — the case and the setup are in service accounts for scheduled jobs, and the full where-do-secrets-live question, for keys and passwords on both platforms, is covered later in this series.
Overlap, Retry, and the Other Edge Cases
What if tonight's run starts while last night's is still going? For transfer jobs this is not academic — a slow link can stretch a one-hour push past a day, and two copies of the same job produce half-written files and doubled uploads.
Cron has no opinion. It will happily start a second, third, and fourth instance on schedule. Overlap protection is yours to add, and the standard tool is flock, which takes a lock file and refuses to start the command if another holder exists:
10 2 * * * flock -n /var/lock/invoice-push.lock \
/opt/jobs/invoice-push/push-invoices.sh >>/var/log/jobs/invoice-push.log 2>&1
Task Scheduler has the rule built in: each task carries an instance policy — do not start a new instance (the default, and the right choice for transfers), queue it, run in parallel, or stop the existing one first. It also offers scheduler-level retry ("if the task fails, restart every ten minutes, up to three times"), which cron again leaves to your script. Scheduler retry is blunt — it re-runs the whole job — so real transfer resilience still belongs inside the job, per our retry and error handling series; but as a free second chance after a transient network blip, the checkbox earns its keep.
Managing Fifty Jobs Instead of Five
At small scale the two tools feel equivalent. At fleet scale their characters diverge.
Cron's plain text is a superpower for management: crontabs diff cleanly, live in version control, and deploy through the same configuration management that ships the scripts. The weakness is visibility — there is no built-in view of "all jobs on this machine and how their last runs went," let alone across machines. Discovering what runs where means reading files: crontab -l per user, plus the system tables.
Task Scheduler inverts the trade. The console shows every task with its status, last run time, and last result — genuine per-machine visibility — and schtasks /query /fo csv /v exports it all for scripts. But the definitions are objects, not text: harder to diff, review, and deploy, with XML export/import as the workable middle ground.
Neither gives you fleet-wide truth, which is why a shop running both needs two things the schedulers do not provide: a jobs register — one human-maintained list of every scheduled flow, wherever it runs — and external monitoring that checks outcomes (did the file arrive?) rather than trusting either scheduler's own report. The register discipline is covered in scheduled job hygiene, and the outcome checks in our transfer job monitoring series. The receiving side has a view of its own, too: a transfer server that logs centrally — Sysax Multi Server, for example, records logins and transfers to file or database — shows every job that actually connected, whichever scheduler launched it, which makes the server log a fine cross-platform reconciliation source.
Before the conventions that tie a mixed estate together, here is everything above in one view:
| Question | cron | Task Scheduler |
|---|---|---|
| Job definition | One line in a plain-text crontab | Task object: triggers, actions, conditions, settings |
| Environment handed to the job | Near-empty: sparse PATH, /bin/sh, no profile | Hidden session: no mapped drives, Start in sets working directory |
| Job output | Mailed to owner/MAILTO; lost without a mailer | Discarded; jobs must write their own logs |
| Missed window (machine off) | Silently skipped; anacron-style tools catch up daily jobs | Skipped unless the run-after-missed-start setting is enabled |
| Run at boot | @reboot entry |
Startup trigger, with optional delay |
| Identity and secrets | Runs as crontab owner; nothing stored | Configured account; password stored for logged-off runs |
| Overlap protection | None — bring your own flock |
Built-in instance policy (skip, queue, parallel, stop) |
| Retry on failure | None — script's job | Restart every N minutes, up to K attempts |
| Managing many jobs | Text: version control and config management friendly; weak visibility | Good per-machine console; XML/schtasks for scripting; harder to diff |
Running Both Without Losing Your Mind
If your transfer estate spans both platforms — most do — a few conventions buy back most of the sanity the split costs:
- One naming convention, both platforms.
invoice-push-nightlyas a Task Scheduler task name andinvoice-push.shin a crontab comment should obviously be relatives. Names are the first thing a responder sees at three in the morning. - One log shape, one log home. Same timestamp format, same one-line-per-event style, a predictable per-job log location on each platform. A responder should not need to remember which OS they are on to read a job log.
- Logic in scripts, schedulers thin. Keep retries, locking, validation, and notification inside the script or tool being scheduled, so the scheduler-specific configuration stays small enough to recreate from the register in minutes.
- One register for everything. A single list of every scheduled flow — machine, platform, schedule, owner, purpose — regardless of which scheduler runs it. Split registers rot in different ways at different speeds.
- Decide misses per flow, not per platform. "Catch up or skip" is a property of the business flow; record the answer in the register and implement it with whichever mechanism the hosting platform offers.
And keep one alternative in view for the Windows side: when the scheduled thing is a transfer, you can skip general-purpose scheduling entirely. A transfer task built in Sysax FTP Automation carries its own schedule, retry behavior, and failure email inside one product over SFTP, FTPS, or FTP — no crontab line, no task object, none of the environment traps this article has cataloged. It does not schedule your backups or your cleanup scripts; for the transfer subset of the estate, though, it collapses the scheduler-plus-script-plus-notification stack into one place.
Which Should Run a Given Flow?
The honest answer is boring: the scheduler on the platform where the job's files and tools already live. Moving a flow across platforms to get a preferred scheduler buys you a migration project and a second environment to debug. Choose based on gravity, then configure whichever scheduler you landed on properly — the Task Scheduler walkthrough for Windows flows, the bash and cron series for Unix ones. Then give both platforms the same operational spine: a missed-window policy from reboots and misfires, and the register and review habits from scheduled job hygiene. The scheduler differences stop mattering once the discipline around them is uniform.
Frequently Asked Questions
Is cron or Task Scheduler more reliable?
Why does my cron job work when I run the script manually?
env -i) and add absolute paths or a PATH line to the crontab; the failure usually reproduces immediately.Does cron run jobs it missed while the machine was off?
Where is Task Scheduler's equivalent of MAILTO?
Can I use the same script on both platforms?
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.
