Skip to main content

Command Palette

Search for a command to run...

Cron Production Setup: What I Actually Built

How a real Yii & HumHub system scheduled work under real operational constraints

Updated
6 min read
Cron Production Setup: What I Actually Built

Up to this point, the series has talked about cron in abstractions: principles, patterns, framework intent. This article steps out of theory and into a specific, imperfect reality.

What follows is not a reference architecture or a recommendation to copy verbatim. It’s a worked example—a production setup built with Yii and HumHub, shaped by constraints that were very real at the time. Some decisions are elegant. Some are pragmatic. All of them were made to keep the system understandable and stable.

That context matters.

Disclaimer
Details in this article are intentionally generalized and anonymized to comply with non-disclosure agreements (NDA). No proprietary code, credentials, or sensitive infrastructure details are disclosed.


Infrastructure Context: Environments and Constraints

The system ran across three environments:

  • Development

  • UAT

  • Production

All three shared the same application codebase and cron model, but they did not share the same operational guarantees.

The most important constraint was this:

In non-production environments, servers were intentionally shut down from 00:00 to 08:00 SGT every day.

This wasn’t an accident or a misconfiguration. It was a cost and resource decision. During those eight hours, no cron jobs ran. No queues were drained. Time-based logic simply paused.

This single constraint influenced nearly every cron-related decision that followed.


The Crontab: Minimal by Design

The production crontab contained exactly two entries:

* * * * * /usr/bin/php /var/www/localhost/htdocs/protected/yii queue/run >/dev/null 2>&1
* * * * * /usr/bin/php /var/www/localhost/htdocs/protected/yii cron/run  >/dev/null 2>&1

That’s it. No hourly jobs. No daily jobs. No per-feature schedules.

Everything else lived inside the application.

This was a conscious rejection of “distributed scheduling” at the OS level. The operating system knew when to trigger. The application knew what to do.


Why Absolute Paths Everywhere

Every path in the crontab is explicit:

  • /usr/bin/php

  • /var/www/localhost/htdocs/protected/yii

This was not stylistic caution; it was survival.

Cron runs with a minimal environment:

  • No shell profiles

  • No augmented PATH

  • No guarantees about working directory

Early tests failed quietly when relying on php without an absolute path. Fixing this once, explicitly, eliminated an entire class of “works in terminal, fails in cron” issues.


Why All Output Is Suppressed

Both commands redirect stdout and stderr to /dev/null:

>/dev/null 2>&1

At first glance, this looks irresponsible. In practice, it was deliberate.

Reasons:

  1. No local mail delivery
    The servers were not configured to send cron mail. Any output would either vanish or accumulate uselessly.

  2. Logging belongs to the application, not cron
    HumHub and Yii already had structured logging. Allowing cron to emit ad-hoc output created fragmentation.

  3. Noise hides signal
    High-frequency jobs (every minute) generate enormous log volume if left unchecked.

Instead, jobs were expected to:

  • Log explicitly via the framework logger

  • Surface failures through monitoring hooks

  • Fail loudly inside the application, not via cron output

Cron was treated as a trigger, not an observer.

This choice trades local visibility for centralized observability. It only works if the application takes logging seriously—which this system did.


Why One-Minute Granularity Was Chosen

Running both cron/run and queue/run every minute was not about speed. It was about predictability.

A one-minute tick created a simple mental model:

  • Async work is processed within ~60 seconds

  • Interval jobs are evaluated at most 60 seconds late

  • No job depends on sub-minute precision

Why not every five minutes? Or every thirty seconds?

Because the system already had:

  • Nightly downtime in non-prod

  • No hard real-time guarantees

  • A need for operational simplicity

One minute was the smallest unit that:

  • Felt responsive enough for users

  • Was easy to reason about

  • Avoided excessive process churn

Anything finer would have increased complexity without meaningful benefit.


Async and Interval Jobs: How They Coexisted

HumHub’s separation between interval jobs and async jobs mapped cleanly onto this setup.

Interval Jobs (cron/run)

Interval jobs handled tasks like:

  • Periodic cleanup

  • Maintenance routines

  • Scheduled checks

Conceptually:

php yii cron/run

Internally:

  • The framework evaluated which jobs were due now

  • Each job decided whether it should run

  • Time-based logic lived in code, not in crontab

This was crucial for environments with downtime. If a job didn’t run at 02:00 because the server was down, it wasn’t “missed” by cron—it was simply evaluated again at the next available tick.

Whether it should catch up was a job-level decision.

Asynchronous Jobs (queue/run)

Async jobs were triggered by events:

  • User actions

  • System changes

  • Background processes

Example (simplified):

Yii::$app->queue->push(new RebuildIndexJob([
    'resourceId' => $id,
]));

And processed by:

php yii queue/run

This decoupling ensured:

  • User requests stayed fast

  • Heavy work happened outside the request lifecycle

  • Failures could be retried or inspected

The queue absorbed variability. Cron merely drained it.


Living With Nightly Downtime

The nightly shutdown in development and UAT introduced a subtle but important behavior:

Interval jobs scheduled during downtime simply did not run.

This was accepted, not fixed.

Why?

Because these environments were not meant to simulate perfect production behavior. They were meant to:

  • Reduce cost

  • Support functional testing

  • Catch logical errors

Trying to “replay” missed jobs on startup would have:

  • Complicated job logic

  • Introduced edge cases

  • Created differences between environments that were harder to reason about

Instead, jobs were designed to be:

  • Idempotent

  • Defensive

  • Capable of tolerating skipped intervals

Production, which did not shut down nightly, behaved as expected. Non-production environments surfaced a different class of problems—often earlier.

This asymmetry was intentional.


Known Limitations (Owned, Not Hidden)

This setup had limits.

  • Latency: Async jobs could take up to a minute to run.

  • Throughput: queue/run executed jobs serially unless scaled horizontally.

  • Visibility: Cron itself provided no direct signal of success or failure.

  • Overlap risk: Long-running jobs required explicit locking.

None of these were accidental. Each was either mitigated elsewhere or accepted as a tradeoff.

Most importantly, the system avoided hidden complexity. There were no background daemons whose state had to be guessed. No invisible schedulers. No environment-specific cron logic.

When something broke, there were only a few places to look.


Why This Setup Worked (For Its Context)

This cron architecture succeeded not because it was optimal, but because it was coherent.

  • Scheduling logic lived in application code

  • Execution logic lived in jobs

  • Cron triggered both, predictably

  • Constraints were acknowledged, not fought

The result was a system that behaved the same way day after day, even when parts of it were unavailable.

That consistency mattered more than raw efficiency.


What This Example Is—and Isn’t

This is not a universal blueprint. In a different context, with:

  • Always-on servers

  • Dedicated workers

  • High-volume queues

…you might make very different choices.

What is transferable is the mindset:

  • Treat cron as infrastructure, not logic

  • Centralize scheduling intent

  • Accept constraints explicitly

  • Prefer boring predictability over clever optimization

That mindset scales better than any specific configuration.

And once you adopt it, cron stops feeling like a liability—and starts feeling like a quiet, dependable part of the system you actually understand.


☰ Series Navigation

Core Series

Optional Extras

Understanding Cron from First Principles to Production

Part 6 of 12

A practical series exploring cron from core concepts and architecture to real-world Yii & HumHub implementations, focusing on background jobs, queues, scheduling tradeoffs, and how time-based systems behave in production.

Up next

HumHub & Yii: Design Intent Behind the Cron Architecture

Why HumHub schedules the way it does—and what its cron model is really optimizing for