
August 25, 2026
14 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Scheduling automated tasks is fundamental to running production web systems, yet misconfigured schedulers cause more silent failures than almost any other infrastructure component. This Ubuntu Cron Jobs Guide provides the exact syntax, debugging workflows, and environment handling required to make scheduled tasks reliable on modern Linux servers. Whether you are maintaining a legacy PHP application or deploying a new Laravel system, understanding how the daemon actually executes your commands prevents the "it works locally but fails on the server" class of bugs that plague deployments.
crond daemon using five-field time expressions and absolute command paths to automate recurring tasks. Reliable scheduling requires explicit environment variables, output redirection for logging, and testing with run-parts or manual execution before trusting the daemon to execute production workloads unattended.For developers building applications in Nepal or managing remote infrastructure, getting this right is non-negotiable. I have seen countless projects where backups stopped silently because a path changed during an OS upgrade, or where email notifications piled up because the mail transfer agent was missing. If you are looking for broader context on server management for applications, my article on hiring a Laravel developer in Nepal covers the full stack including infrastructure expectations, but this guide focuses strictly on the scheduler itself.
How do you correctly write Ubuntu Cron Jobs syntax?
The most common point of failure in any Ubuntu Cron Jobs Guide is the assumption that the shell environment inside cron matches your interactive terminal. It does not. Cron executes commands with a minimal environment: typically only HOME, LOGNAME, SHELL=/bin/sh, and PATH=/usr/bin:/bin. Any command outside that restricted path, or any dependency on environment variables set in your .bashrc, will fail silently.
The Five-Field Time Expression
Cron uses five whitespace-separated fields to define the schedule. The format is minute, hour, day-of-month, month, and day-of-week. A frequent mistake is confusing the day-of-month and day-of-week fields; setting both to specific values (not *) creates an OR condition in standard Vixie cron, meaning the job runs if either matches. This trips up many developers who intend AND logic.
# ┌───────────── minute (0 - 59)
# │ ┌───────────── hour (0 - 23)
# │ │ ┌───────────── day of month (1 - 31)
# │ │ │ ┌───────────── month (1 - 12)
# │ │ │ │ ┌───────────── day of week (0 - 7, Sun=0 or 7)
# │ │ │ │ │
# * * * * * command-to-execute
# Run daily at 3:30 AM Nepal Time
30 3 * * * /usr/bin/php /var/www/app/artisan schedule:run >> /var/log/app-cron.log 2>&1
# Run every Monday at 9:00 AM
0 9 * * 1 /usr/local/bin/backup-script.sh
# WRONG: Runs on 1st/15th OR Mondays (not AND)
0 9 1,15 * 1 /some/command Absolute Paths Are Mandatory
Never rely on $PATH resolution inside crontab. Always specify the full path to binaries and scripts. On Ubuntu 24.04 LTS, PHP might be at /usr/bin/php8.3 or /usr/bin/php depending on your installation method. Verify with which php or readlink -f $(which php) in your interactive shell, then hardcode that result. For Node.js applications using NVM, the binary path includes the version directory and must be resolved explicitly since NVM modifies PATH dynamically in interactive shells only.
Environment Variable Declaration
You can declare environment variables directly at the top of your crontab file before any schedule entries. This is preferable to sourcing profile scripts, which introduces interactive shell dependencies and unpredictable side effects. Set only what the command actually needs:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
APP_ENV=production
DB_HOST=127.0.0.1
# Now commands inherit these variables
0 2 * * * /usr/bin/php /var/www/app/artisan db:backup On production systems I maintain, I keep environment configuration in the application's .env file and ensure the cron command loads it explicitly rather than duplicating secrets in crontab. For Laravel applications, the framework loads .env automatically when you call artisan with the full path, but custom PHP scripts may need dotenv or explicit variable passing.
Why are my Ubuntu Cron Jobs not running or failing silently?
Silent failure is the default behaviour of cron. If a command exits non-zero, produces no output, and you have not configured MAILTO, you will never know it failed. Debugging requires systematic elimination of the four most common causes: path issues, permission problems, environment mismatches, and timing conflicts.
Check the Cron Logs First
Ubuntu logs cron activity to /var/log/syslog by default (or /var/log/cron.log if rsyslog is configured to separate it). Filter for cron entries to see whether the daemon attempted execution:
# View recent cron executions
grep CRON /var/log/syslog | tail -50
# Check for permission denied or missing binary errors
grep -i "cron\|error\|denied" /var/log/syslog | tail -100
# Enable verbose cron logging temporarily
# Edit /etc/rsyslog.d/50-default.conf
# Uncomment: cron.* /var/log/cron.log
sudo systemctl restart rsyslog If syslog shows the command executed but your application did not behave as expected, the problem is inside the command itself, not the scheduler. Redirect both stdout and stderr to a log file to capture runtime errors:
# Capture all output for debugging
0 3 * * * /usr/bin/php /var/www/app/artisan sync:orders >> /var/log/sync-orders.log 2>&1
# Include timestamp for correlation
0 3 * * * echo "$(date '+\%Y-\%m-\%d \%H:\%M:\%S') Starting sync" >> /var/log/sync.log && /usr/bin/php /var/www/app/artisan sync:orders >> /var/log/sync.log 2>&1 Note the escaped percent signs in the date format string. Cron interprets unescaped % as newline characters, which breaks the date command and causes silent failures. This is one of the most frequently missed details in any Ubuntu Cron Jobs Guide.
Permission and Ownership Issues
Cron runs as the user whose crontab owns the entry. If your script writes to /var/www/app/storage/logs but the crontab belongs to root while the web server runs as www-data, you create permission conflicts that manifest as intermittent failures. Always match the cron user to the application owner. On shared hosting or multi-tenant servers, verify ownership with ls -la on target directories and ensure the cron user has write access.
File permissions on the script itself matter too. Scripts must be executable (chmod +x script.sh) if invoked directly, or you must invoke them through an interpreter explicitly (/bin/bash /path/to/script.sh). I prefer the latter approach because it removes ambiguity about shebang lines and works consistently across different deployment methods.
Testing Without Waiting
Never deploy a new cron entry and wait for the scheduled time to verify it works. Test immediately using three methods:
- Manual execution: Copy the exact command from crontab and run it in a clean environment:
env -i /bin/bash --noprofile --norc -c 'your-command-here'. This simulates cron's minimal environment. - Run-parts validation: For scripts in
/etc/cron.dailyor similar directories, test withrun-parts --test /etc/cron.dailyto verify naming conventions (no dots, no extensions) allow execution. - Minute-level test: Temporarily set the schedule to
* * * * *(every minute), watch the log for two successful executions, then change to the intended schedule. Never leave a per-minute job active in production.
How do you manage Laravel Scheduler with Ubuntu Cron?
Laravel's task scheduler exists specifically to avoid editing crontab for every new scheduled task. Instead of managing multiple cron entries, you add a single entry that invokes the scheduler every minute, and define all tasks in app/Console/Kernel.php (Laravel 10 and earlier) or routes/console.php (Laravel 11+). This is the pattern I use on every Laravel project I ship, from legal-tech portals like Court Marriage In Nepal to eCommerce platforms like Nepal Gift Card.
The Single Cron Entry
Add exactly one entry to your application user's crontab:
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1 The cd is critical. Artisan commands often assume they run from the project root, and without changing directory first, relative paths in your scheduled tasks will break. The output redirect to /dev/null prevents cron from emailing you every minute; Laravel handles its own logging through the framework's log channels.
Defining Scheduled Tasks
In Laravel 12 (current stable as of 2026), scheduled tasks are defined in routes/console.php using the Schedule facade:
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send-digest')->dailyAt('08:00');
Schedule::command('reports:generate')->weeklyOn(1, '06:00');
Schedule::call(function () {
// Closure-based tasks for simple operations
Cache::flush();
})->hourly();
// Prevent overlapping executions
Schedule::command('sync:inventory')->everyFiveMinutes()->withoutOverlapping();
// Run only on specific environments
Schedule::command('billing:charge')->monthly()->onOneServer(); The withoutOverlapping() method is essential for tasks that might run longer than their interval. Without it, a slow inventory sync that takes seven minutes will spawn a second instance while the first is still running, causing race conditions and duplicate processing. Laravel uses cache locks to prevent this, so ensure your cache driver (Redis recommended) is properly configured.
Server-Level Considerations
When deploying Laravel applications on Ubuntu 24.04 with PHP 8.3 or 8.4, ensure the PHP CLI binary matches your FPM version. Running php -v should show the same major version as your web server. Mismatches cause subtle bugs where scheduled tasks use different extensions or ini settings than web requests. On servers managed with Deployer 7, I symlink the release directory and ensure the cron entry points to the current symlink path, which survives zero-downtime deployments without crontab edits.
For applications requiring precise timing or distributed execution across multiple servers, consider Laravel's onOneServer() method combined with Redis or Memcached. This ensures only one server in a cluster executes the task, preventing duplicate charges or notifications. The cache driver must support atomic locks; file and database drivers do not provide reliable locking under concurrency.
What are the security and maintenance best practices for production cron?
Treating cron as infrastructure-as-code rather than ad-hoc system configuration separates reliable systems from fragile ones. Every scheduled task should be auditable, monitored, and recoverable.
Least Privilege Execution
Never run application cron jobs as root unless absolutely necessary. Create a dedicated service user or use the application owner account. Root-level cron entries should be reserved for system maintenance (log rotation, package updates, certificate renewal). Application tasks like queue workers, report generation, and data syncs should execute with the minimum permissions required. This limits blast radius if a script is compromised or contains a bug that deletes files.
Monitoring and Alerting
Cron's native email notification is insufficient for production monitoring. Implement health checks using one of these patterns:
- Dead man's switch: Use external services like Healthchecks.io or Cronitor. Your cron job pings a unique URL on success; the service alerts you if the ping stops arriving. This catches both command failures and server-level issues like cron daemon crashes.
- Log-based monitoring: Configure Promtail/Filebeat to ship cron logs to Loki/Elasticsearch, then alert on absence of expected log patterns. This integrates with existing observability stacks.
- Application-level heartbeats: Write a timestamp to Redis or database on each successful execution. A separate monitoring job checks freshness and alerts via Slack/email/PagerDuty if stale.
On client projects where budget constraints rule out paid monitoring services, I implement the heartbeat pattern using Redis. A simple Redis::set('cron:last-sync', now()) at the end of each task, checked by a separate lightweight monitor, costs nothing and catches failures within minutes.
Idempotency and Recovery
Design every scheduled task to be safely re-runnable. If a payment processing job fails halfway through, running it again should not create duplicate charges. Use database transactions, idempotency keys, or status flags to track progress. When a task fails, the next execution should pick up where the previous left off rather than restarting from scratch or skipping incomplete records.
| Practice | Risk If Ignored | Implementation Effort |
|---|---|---|
| Absolute paths everywhere | Silent failures after OS/package upgrades | Low — verify once per deployment |
| Output redirection to logs | No visibility into failures | Low — append to every cron line |
| Dedicated non-root user | Privilege escalation, accidental system damage | Medium — initial setup only |
| External health monitoring | Undetected failures for days/weeks | Medium — integrate once per project |
| Idempotent task design | Duplicate operations, data corruption | High — requires architectural planning |
| Version-controlled schedules | Configuration drift, unreproducible deploys | Low — use Laravel Scheduler or Ansible |
Log Rotation and Disk Space
Cron jobs that append to log files without rotation will eventually fill the disk, causing cascading failures across the entire server. Configure logrotate for every application log that cron writes to:
# /etc/logrotate.d/myapp-cron
/var/log/myapp-cron.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 0640 appuser appgroup
} Test rotation with logrotate -d /etc/logrotate.d/myapp-cron before deploying. The -d flag performs a dry run without modifying files. On production servers I maintain, disk space exhaustion from unrotated logs is among the top three causes of unplanned downtime, entirely preventable with fifteen minutes of configuration.
How does Ubuntu Cron compare to systemd timers and other schedulers?
While cron remains the default choice for simple recurring tasks, systemd timers offer advantages for complex scenarios. Understanding when to use each prevents over-engineering simple jobs or under-engineering critical ones.
Systemd timers provide dependency management (wait for network/database), randomized delays to prevent thundering herd on boot, monotonic timing (run X minutes after last completion rather than at fixed clock times), and integrated journal logging. They are superior for system maintenance tasks with complex requirements. However, they require two unit files per task (service + timer), lack the simplicity of a single crontab line, and are harder to manage per-user. For application-level scheduling in Laravel, Symfony, or Node.js ecosystems, application-native schedulers remain the better choice because they understand framework context, database connections, and cache locks.
Cron persists as the pragmatic default because it is universally available, requires no additional configuration for basic use, and every Linux administrator understands it. The decision matrix is straightforward: use your application's scheduler for business logic, cron for simple system tasks, and systemd timers only when you need features cron cannot provide. Over-engineering the scheduler creates maintenance burden without improving reliability.
Making Ubuntu Cron Jobs Reliable in Production
This Ubuntu Cron Jobs Guide has covered the technical mechanics, but reliability ultimately comes from treating scheduled tasks with the same rigor as application code. Use absolute paths, declare environments explicitly, redirect output to rotated logs, implement health monitoring, and test every new entry before trusting it. For Laravel applications, delegate to the framework scheduler and maintain only a single cron entry. These patterns have proven reliable across hundreds of production deployments I have managed over fifteen years.
If you are struggling with silent cron failures, need help migrating legacy scheduled tasks to a modern architecture, or want to audit your existing scheduler configuration for production readiness, reach out through my contact page. I regularly help teams in Nepal and worldwide untangle scheduling issues that have been causing intermittent problems for months. You can also explore my full-stack development services for comprehensive application and infrastructure support, or read my guide on DevOps automation in Nepal for broader infrastructure patterns beyond cron.

