On the same machine, two Node.js scripts run in the background. The first publishes to dev.to at 9am and LinkedIn at 10am — a cron, three lines of config. The second watches a job queue every 30 seconds, keeps state in memory, and reacts to user interface actions within seconds — a daemon supervised by systemd.
Same language, same server, apparently the same goal: run tasks in the background. Yet the choice is different. It's not a matter of taste or habit — it's that the two problems only look similar from a distance.
The simple case: a cron for publishing
The publishing cron is deliberately boring. A user crontab entry:
0 9 * * * bash /home/folken/work/cv/scripts/devto-cron.sh >> logs/devto-cron.log 2>&1
0 10 * * * /usr/bin/node /home/folken/work/cv/scripts/linkedin-cron.js >> logs/linkedin-cron.log 2>&1
The shell script loads environment variables from a .env file
and calls the Node.js script. That script reads devto-schedule.json,
picks the first article with drafted status, calls the dev.to API,
updates the file, and exits. Runtime: a few seconds.
On failure, the log contains the error, the article stays in queue for the next day.
Why does cron work here? Because the task is atomic. It doesn't need to know what happened yesterday. It doesn't need to react to an external event. It won't overlap with itself. A process starts, does its job, stops. That's exactly what cron was designed for.
Where cron starts to fall short
The automated monitoring system poses a different problem. There are four active monitors (crypto, tech, Epstein, retro), each with its own frequency (from 6 hours to 7 days). The admin interface allows manually triggering a monitor from the browser. A "generate" job can be queued at any time to reconfigure a monitor via Claude.
With a cron, you quickly run into several problems:
- Minimum 1-minute granularity. Can't react in 5 or 10 seconds to a job queued from the UI. The user clicks, waits, sees nothing happen.
- No state between runs. To know when each monitor last ran, you need to read it from a file at each startup. Not catastrophic, but it complicates coordination between multiple monitors sharing the same resources.
- No job queue. If two crons trigger simultaneously and
both try to write the same files, you need to handle overlap with
flockor equivalent — which works, but requires attention. - No native structured logging. Stdout redirected to a file becomes unreadable quickly when multiple monitors write to the same log in parallel.
None of these limits are individually insurmountable. But stacked together, they signal that you're trying to make cron do something outside its domain.
The daemon: a process that never sleeps
veille-daemon.js runs continuously. It polls jobs.json
every 30 seconds for on-demand jobs, and cron-config.json every 60
seconds to trigger scheduled monitors whose frequency has elapsed.
It keeps the monitor registry and current run state in memory.
When the user clicks "Rerun" in the interface, PHP writes a job to
jobs.json. The daemon picks it up at the next poll (30 seconds worst case),
executes it, and updates the status. The interface can display progress in real time
via a /veille/status endpoint.
systemd supervises the daemon with a minimal .service file:
[Unit]
Description=Veille daemon
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/folken/work/cv
ExecStart=/usr/bin/node scripts/veille-daemon.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=default.target
What this concretely provides: automatic restart on crash,
logs in journald (journalctl --user -u veille-daemon -f),
automatic startup on boot with systemctl --user enable veille-daemon.
No more manual restart scripts, no more separate log file to watch.
systemd timer: the middle ground
There's a third option that often gets overlooked: the systemd timer.
It's essentially an enhanced cron — a periodic task, but supervised by systemd.
On this project, crypto-veille.timer is a legacy example:
# crypto-veille.service
[Unit]
Description=Crypto veille job
After=network.target
[Service]
Type=oneshot
WorkingDirectory=/home/folken/work/cv
ExecStart=/usr/bin/node scripts/crypto-veille.js
# crypto-veille.timer
[Unit]
Description=Crypto veille — every 6h
[Timer]
OnBootSec=5min
OnUnitActiveSec=6h
[Install]
WantedBy=timers.target
Compared to a cron, the systemd timer adds: native logs in journald,
the ability to declare dependencies (After=network.target),
a boot delay with OnBootSec (cron can fire too early if the machine
just rebooted), and more readable calendar expressions than classic cron syntax.
The trade-off: two files to manage instead of one line. For a simple task that doesn't need structured logs and whose minimum frequency is 1 minute, cron remains more direct.
Comparison table
| cron | systemd timer | daemon | |
|---|---|---|---|
| Granularity | 1 min minimum | 1 second | free (polling) |
| Logs | manual file | native journald | native journald |
| State between runs | none | none | in memory |
| On-demand reaction | no | no | yes |
| Crash supervision | no | yes | yes |
| Config complexity | very low | medium (2 files) | high (code to write) |
| Ideal use case | atomic periodic task | periodic task + logs/deps | job queue, state, on-demand |
Three questions to choose
In practice, three questions are enough to guide the choice:
1. Does the task need to react to an event in under a minute?
If yes: daemon. Cron can't do better than one minute, and neither can a systemd timer
(it runs scheduled tasks, not reactive ones).
2. Does it need state between runs?
If yes: daemon. Reading and writing a file at each run works up to a point,
but once there are multiple workers or decisions to make based on recent history,
a continuous process with in-memory state is cleaner.
3. Do structured logs and supervision matter?
If yes but the first two answers are no: systemd timer.
It gives you the systemd benefits (journald, restart, dependencies) without
the complexity of writing an event loop.
If all three answers are no: cron. Three lines in the crontab, a redirect to a log file, zero additional infrastructure. Don't over-engineer what doesn't need it.
Conclusion
Both approaches coexist without friction on the same machine. The cron has been publishing articles on schedule for years without ever needing a manual restart. The monitoring daemon took a few days of work to make robust — crash handling, TTL on stuck states, recovery after restart.
It's not cron OR daemon. It's recognizing which one matches the problem at hand. Fixed-schedule publishing doesn't need to react in 30 seconds. Interactive monitoring can't afford to wait a minute between checks. The choice is in the problem, not the technology.