blog

PHP Task Scheduling in Webrium: One Cron Entry, Every Job

Sep 24, 2026 · Updated Sep 25, 2026
PHP Task Scheduling in Webrium: One Cron Entry, Every Job

Every PHP application eventually collects a pile of background chores: nightly reports, temp-file cleanup, weekly newsletters. The classic answer is cron, and the classic outcome is a crontab nobody remembers. It gets edited over SSH, lives outside version control, and quietly disappears the next time you migrate servers.

Webrium's new task scheduler moves that schedule into your codebase. You describe what should run and when in PHP, and a single cron entry does the rest.

The Problem with One Crontab Line per Job

Managing recurring work directly in crontab has a few well-known pain points:

  • The schedule lives on the server, not in your repo. A deploy doesn't carry it with it.
  • Every new job means editing the server. That's easy to forget and hard to review.
  • Nothing stops a slow job from overlapping with itself. If a 5-minute task takes 7 minutes, you now have two copies running.
  • It's hard to answer "when does this run next?" without doing cron math in your head.

How Webrium's Scheduler Works

You add one line to your server's crontab, once, and never touch it again:

* * * * * cd /var/www/your-app && php webrium schedule:run >> /dev/null 2>&1

Every minute, schedule:run loads the task files in app/Schedules, works out which tasks are due at that moment, runs them, and exits. Add a new task next month and there is nothing to change on the server. The next tick picks it up automatically.

Your First Scheduled Task

Scaffold a task file with the console:

php webrium make:schedule SendReports

This creates app/Schedules/SendReports.php. Register your task with Schedule::call():

<?php

use Webrium\Schedule;

Schedule::call('App\Services\ReportService@generateDaily')
    ->name('reports.daily')
    ->dailyAt('01:00');

Schedule::call() accepts a closure, an array callable, a global function name, or a 'Class@method' string. With the string form, use the fully qualified class name; Webrium creates the class without constructor arguments and calls the method.

Give important tasks a name with ->name(). The name identifies the task in reports and CLI commands, and it also keys the task's overlap lock.

Choosing When a Task Runs

The fluent API reads like plain English:

Schedule::call($cleanup)->name('cleanup.temp')->everyFiveMinutes();
Schedule::call($stats)->name('stats.hourly')->hourlyAt(15);
Schedule::call($digest)->name('digest.weekly')->weeklyOn(1, '08:00'); // Mondays, 08:00

For anything more specific, pass a standard five-field cron expression:

// Every 15 minutes, 9am–5pm, Monday to Friday
Schedule::call($sync)->name('crm.sync')->cron('*/15 9-17 * * 1-5');

The full list of frequency methods is in the Task Scheduling docs.

Develop and Debug Without Waiting for Midnight

The console ships three commands that make working with scheduled tasks pleasant:

php webrium schedule:list                # see every task and when it's next due
php webrium schedule:test reports.daily  # run one task right now
php webrium schedule:work                # run the scheduler locally, no crontab needed

schedule:list prints a table you can trust before you go hunting through logs:

+---------------+-------------+------------------+
| Task          | Expression  | Next Due         |
+---------------+-------------+------------------+
| cleanup.temp  | */5 * * * * | 2026-09-19 12:05 |
| reports.daily | 0 1 * * *   | 2026-09-20 01:00 |
+---------------+-------------+------------------+

schedule:work is meant for local development. In production, stick with the real cron entry, which the operating system keeps running even after a crash or reboot. See the console reference for the details.

Built-In Safety Nets

Scheduled tasks run unattended, so the scheduler is careful about failure:

  • Overlap prevention. Each task takes a file lock before it runs. If the previous run is still going when the next tick arrives, the new attempt is skipped and reported instead of piling up. Because it uses an OS-level flock, the lock is released automatically if a process dies.
  • Failure isolation. If one task throws, the error is logged and the remaining due tasks still run.
  • Broken files don't break the schedule. A syntax error in one task file is reported, and every other file still loads.
  • Honest exit codes. schedule:run exits with 0 when everything succeeded and 1 otherwise, so cron mail-on-error and monitoring tools keep working.

Each run prints a clear one-line report per task:

✔ reports.daily
⚠ cleanup.temp skipped (already running)
✘ newsletter.weekly failed: Connection timed out

One File per Task, Plugin-Friendly by Design

Tasks live in app/Schedules/, and Webrium loads every .php file there recursively. That means a plugin can drop its own task into a subfolder such as app/Schedules/my-plugin/SendWelcomeEmail.php without editing anyone else's file and without any risk of a name collision.

Things to Keep in Mind

  • Locks are per server. Two servers with separate filesystems will each hold their own lock. On a horizontally scaled setup, run the scheduler on one designated server, or make your tasks safe to run more than once.
  • Tasks run one after another. Due tasks execute sequentially within a single schedule:run, so keep tasks short and hand heavy work off elsewhere.
  • Times follow PHP's default timezone. Check your date.timezone setting or set it in your app so "01:00" means the 01:00 you expect.
  • Resolution is one minute. Scheduling follows standard five-field cron syntax.

Getting Started

  1. Add the single cron entry to your server.
  2. Run php webrium make:schedule YourTask.
  3. Define the task with Schedule::call().
  4. Check it with schedule:list and try it with schedule:test.

One line in crontab, one small PHP file per task, and a schedule that finally lives in your repository. For every method and option, head to the Core and Console documentation.

FAQ

Do I need a separate cron entry for each task? No. One entry running php webrium schedule:run every minute is enough for all of your tasks.

Can I run a task more than once a minute? No. Scheduling follows five-field cron syntax, so the finest interval is one minute.

What happens if a task is still running when it's due again? The new attempt is skipped and reported as skipped (already running). Your other due tasks continue as normal.

Does it work across multiple servers? The overlap lock is single-server. For multi-server deployments, run the scheduler on one server or make tasks safe to run concurrently.

More from blog

View all →