Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Crontab Laravel Package

workerman/crontab

workerman/crontab is a lightweight cron scheduler for Workerman PHP applications. It adds second-level cron expressions and recurring task scheduling to Workerman workers, letting you run background jobs on timers without relying on external cron.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install: composer require workerman/crontab (pulls workerman/workerman). PHP 7.4+ CLI SAPI required — this is a long-running process, not a request/response library. On PHP 8.4, require ^1.0.7 — earlier tags throw runtime errors on 8.4 (fixed in v1.0.7, PR #17); pin "workerman/crontab": "^1.0.7" if your fleet is on 8.4, otherwise ^1.0 is fine.
  • Read first: README.md for the expression grammar, then src/Crontab.php (where onWorkerStart registers the internal tick timer and callbacks fire) and src/Parser.php (the actual field grammar — this is where you'll go when a rule silently never fires).
  • Biggest mental adjustment: this is 6 fields, seconds first, not system cron's 5. */5 * * * * * = every 5 seconds; 0 * * * * * = every minute at second 0.
  • Minimal working entry point:
<?php
require __DIR__ . '/vendor/autoload.php';

use Workerman\Worker;
use Workerman\Crontab\Crontab;

date_default_timezone_set('UTC');

$worker = new Worker();          // no listener needed
$worker->name = 'cron';
$worker->count = 1;

$worker->onWorkerStart = function () {
    new Crontab('*/5 * * * * *', function () {
        echo date('Y-m-d H:i:s') . " heartbeat\n";
    });
};

Worker::runAll();
  • Run foreground while developing: php cron.php start. Add -d only for production. The first realistic use case is a heartbeat/pruning job (e.g. delete expired sessions, flush temp tables), not a replacement for a real job queue.

Implementation Patterns

  1. One dedicated scheduler worker, count = 1. This is the pattern to default to. All crontabs register inside onWorkerStart, so reload re-registers them cleanly:
$worker = new Worker();
$worker->count = 1;
$worker->onWorkerStart = function () {
    new Crontab('0 * * * * *',    [Jobs\TickMinute::class,  'run']);
    new Crontab('0 0 * * * *',    [Jobs\TickHour::class,    'run']);
    new Crontab('0 0 3 * * *',    [Jobs\TickNightly::class, 'run']);
    new Crontab('0 0 0 1 * *',    [Jobs\TickMonthly::class, 'run']);
};
  1. Callback parameterization. The callback receives the Crontab instance, so use use for injected services (container, logger, config) rather than globals:
new Crontab('*/10 * * * * *', function (Crontab $cron) use ($container) {
    $container->make(ReportJob::class)->handle();
}, [], 'reports');   // 4th arg sets an id you can remove later
  1. One-shot / self-removing jobs. Return false to have the scheduler remove the entry:
new Crontab('*/1 * * * * *', function () use ($deadline) {
    if (time() >= $deadline) {
        return false;   // destroys this crontab
    }
    doDeferredWork();
});

Also $cron->destroy() and (by id) Crontab::remove('reports') for external cancellation.

  1. Isolate blocking jobs. The tick loop is single-threaded per process. If job A takes 30s but fires every 5s, job B on the same worker gets starved. Either put each long job on its own Worker (count = 1, own crontab), or have the crontab only enqueue to Redis/Beanstalk and let real workers consume. The latter is nearly always the right answer.

  2. Laravel integration — do not replace schedule:run with this. Run it as a Supervisor-managed Artisan command that dispatches the same jobs:

class CronWorkerCommand extends Command
{
    protected $signature = 'cron:work {action=start}';

    public function handle()
    {
        $kernel = $this->laravel;
        $worker = new Worker();
        $worker->name = 'cron';
        $worker->count = 1;
        $worker->onWorkerStart = function () use ($kernel) {
            new Crontab('* * * * * *', function () use ($kernel) {
                $kernel->call('schedule:run');
            });
        };
        Worker::$pidFile    = storage_path('app/cron.pid');
        Worker::$logFile    = storage_path('logs/cron.log');
        Worker::$stdoutFile = storage_path('logs/cron.out');
        Worker::runAll();
    }
}
  1. Observability. Keep a heartbeat crontab that writes a "last run" timestamp to Redis/file, and log start, duration, id, result around every callback. A stale heartbeat is your only early warning that the worker died.

  2. Rollout/rollback. Use php start.php reload (re-runs onWorkerStart, re-registers crontabs) rather than restart — restart drops in-flight jobs mid-callback. Under Supervisor, prefer stopwaitsecs long enough to let the current tick finish.

  3. Pin and upgrade deliberately. The upgrade path is a composer update workerman/crontab + php start.php restart — reload won't pick up new vendor code if you're on an opcache-less long-running process (it does, since PHP re-includes on onWorkerStart, but restart is the safe habit for library bumps). v1.0.6 → v1.0.7 is a patch-only change (PHP 8.4 runtime error fix); no API, grammar, or behavior changes, so no code edits are expected.

Gotchas and Tips

  • Six fields, seconds first. Copy-pasting a line from /etc/crontab or Laravel's schedule will produce a wrong schedule or a parse error. @daily-style macros aren't supported by the parser either.
  • Requires a live Workerman event loop. new Crontab(...) outside Worker::runAll() simply never fires. There is no facade, no service provider, no automatic registration.
  • count > 1 duplicates every job. Each worker process registers its own Crontab and fires independently — two processes = two executions per tick. Keep scheduler workers at count = 1 unless duplication is what you actually want.
  • Blocking callbacks stall all timers on that process. No concurrency within a worker. Long I/O in a */5 * * * * * job will delay everything else.
  • No overlap protection. If a run overruns its interval, the next tick queues and you may get back-to-back executions. Guard stateful jobs with a lock (Redis SET NX PX, filesystem flock, DB advisory lock).
  • Uncaught exceptions. They bubble into the event loop; depending on how your callback is written, the worker can die and take the schedule down with it. Wrap every callback body in try/catch and log. Check Worker::$stdoutFile when things look silent.
  • Timezone. The parser and tick check use PHP's default timezone. Call date_default_timezone_set() at the top of the entry script before Worker::runAll(). A UTC default in production silently shifts every hourly/daily rule.
  • Resolution is ~1 second with drift. Don't rely on sub-second precision or exact wall-clock alignment on high-frequency jobs. The internal tick is an interval timer, not an alarm on second boundaries.
  • Expression grammar is limited. Supported tokens: *, */n, n, n-m, n,m,..., and step variants. Day-of-week: 0 and 7 both mean Sunday. Validate every rule in a unit test — a bad expression may throw from Parser only at registration time inside onWorkerStart, which is easy to miss in daemon mode.
  • Memory creep in long-lived workers. Closures that capture large objects or persistent container state prevent GC. Avoid capturing request-scoped objects; reuse DB/Redis connections; consider a scheduled self-reload (php start.php reload from a system-level cron or a nightly posix_kill) if leaks are hard to eliminate.
  • Returning false silently destroys. Great for one-shots, terrible when left in accidentally — the job runs once and then nothing. Pair it with an explicit condition and a log line.
  • PHP 8.4 users must be on v1.0.7. On v1.0.6 and below, the library errors at runtime on 8.4 (PR #17). Symptoms look like a dead scheduler rather than a version problem — check composer show workerman/crontab before debugging expressions or timezones. Conversely, if you're on PHP 7.4/8.0–8.3, v1.0.6 and v1.0.7 are functionally identical, so there's no urgency to bump.
  • Debugging workflow. Start foreground, add echo/Worker::log(). php start.php status shows worker state; php start.php connections is irrelevant here. Per-job duration logging is the fastest way to find a slow job starving the loop.
  • Extension points. Subclass Crontab and override checkRule (or wrap Parser) to add @daily/@every 5m macros, per-rule metrics, or feature flags. Crontab::getAll() enumerates registered jobs from inside the same process if you want a health endpoint.
  • Missing semantics you'll want from real cron. No MAILTO, no env vars, no retries, no durable missed-run catch-up — if the process is down at tick time, the run is gone. For at-least-once, persisted, retryable behavior, use a queue; let this package only enqueue.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource