Developer Tools

Cron Expression Generator: The Five Fields and the OR Rule

The five cron fields, the four special characters, and the rule that catches everyone: day of month and day of week are combined with OR, not AND. Plus why WP-Cron misses schedules, and the two lines that put WordPress on a real timer.

Cron Expression Generator: The Five Fields and the OR Rule

You want a job to run at quarter past three every morning. You type 15 3 * * *, then stop, because you cannot remember whether the third field is the day of the month or the day of the week, and whether Sunday is 0 or 7. Five numbers, no labels, no confirmation. The only feedback cron gives you is silence, at the wrong time or forever.

Then there is the WordPress complication. WordPress has its own scheduler, WP-Cron, and it does not speak cron syntax at all. Worse, it has no timer. It runs when somebody loads a page. A site with no visitors between midnight and six runs nothing between midnight and six, which is the entire reason a post scheduled for 02:00 is still sitting in the list at breakfast, marked as missed.

Two questions are tangled together every time: what does this line of five fields mean, and which scheduler is going to honour it. The cron expression generator below answers the first out loud, in plain English, with the next ten runs printed in your clock and in UTC. It also writes the WordPress code, because in WordPress the expression is only half the job.

Type an expression into the top field and it splits across the five below. Change any one of the five and the expression is rebuilt. Everything happens in your browser: nothing is uploaded anywhere, and the page makes no network calls at all.

Cron expression builder

Read a cron expression back in plain English, or build one from the five fields and watch the next ten runs appear in your local time and in UTC side by side. It marks the mistakes that cost a schedule its job, and writes the WordPress and the real crontab version of it. Every date is worked out in this browser tab, nothing is uploaded and no library is loaded.

0 to 59

0 to 23, midnight is 0

1 to 31

1 to 12, or JAN to DEC

0 to 7, or SUN to SAT. 0 and 7 are both Sunday

Every field takes a list (1,15), a range (9-17), a step (*/15 or 9-17/2), a name (MON-FRI) and the wildcard (*), in any combination.

Start from

At every 15th minute past hours 9 through 17 on Monday through Friday.

What this schedule gets wrong
    The next ten runs
    Local timeUTC

    The same thing in WordPress

    PHP For functions.php or a small plugin of your own
    crontab The real cron job that replaces WP-Cron, run crontab -e as the site user
    wp-config.php Above the line that says that is all, stop editing
    WunderPaint
    The Dynamic Design and Automation Studio
    WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

    The five fields, in order

    Minute, hour, day of month, month, day of week. Separated by spaces, read left to right, evaluated against the clock the daemon runs on. That last point catches people out on hosting where the server keeps UTC while the WordPress settings screen says Europe/Berlin, which is why the builder has a switch for which clock to read the expression in, and prints both columns side by side.

    The ranges are not symmetrical, and that is where half the mistakes live. Hours run 0 to 23, so midnight is 0 and there is no 24. Days of the month run 1 to 31, months run 1 to 12, and days of the week run 0 to 7 with both 0 and 7 meaning Sunday. Months and weekdays also accept three letter names, case insensitive.

    Table of the five cron fields: minute 0 to 59, hour 0 to 23, day of month 1 to 31, month 1 to 12 with names JAN to DEC, day of week 0 to 7 with names SUN to SAT, each with the mistake it invites, and the two day fields highlighted because they combine with OR.

    A sixth field at the front means seconds, which belongs to Quartz rather than to cron. A sixth field at the end means something else entirely: in /etc/crontab and under /etc/cron.d it names the user the job runs as. That is why a line copied out of a personal crontab into /etc/cron.d fails, with the day of week field read as a username.

    Four characters and a handful of names

    • * means every value the field allows.
    • , builds a list: 0,15,30,45 in the minute field.
    • - builds a range: 9-17 in the hour field is nine values, 9 through 17 inclusive, so a job on that range still fires at 17:00.
    • / applies a step to a range: */15 in the minute field gives 0, 15, 30 and 45.

    The step is the one that gets misread. It is not “every fifteen minutes from now”, it is a filter over the values the field already contains, counted from the start of the range. So */40 in the minute field does not mean every forty minutes: it means minute 0 and minute 40, then a twenty minute gap while the hour rolls over. A genuine forty minute cycle cannot be written as a cron expression.

    The form 5/10, a step hung off a bare number rather than a range, is the other trap. Some daemons read it as “from 5 to the end of this field, every 10”, others reject it outright. The builder flags it as non-portable rather than guessing for you. Write 5-59/10 and it means the same thing everywhere. A step wider than its own range gets flagged too, because 0-10/20 is a long way of writing 0.

    Names are convenient and slightly treacherous. JAN through DEC and SUN through SAT work everywhere. Ranges of names such as MON-FRI are accepted by the cron most Linux distributions ship today, but the manual page inherited from Vixie cron still says they are not allowed, so write 1-5 if the line has to move to a machine you do not control. The nicknames @hourly, @daily (also @midnight), @weekly, @monthly, @yearly and @annually expand to ordinary expressions, and the builder expands them so you can see what you agreed to. @reboot is the odd one out: an event, not a schedule.

    Day of month and day of week are an OR

    This is the rule that surprises everyone, including people who have written crontabs for twenty years. If you restrict both the day of month field and the day of week field, cron runs the job when either matches, not when both do.

    So 0 0 13 * 5 is not “midnight on Friday the 13th”. It is “midnight on the 13th of every month, and also midnight every Friday”, which is around sixty runs a year instead of the one or two you had in mind. The example in the manual is 30 4 1,15 * 5: half past four on the 1st, on the 15th, and on every Friday.

    The switch between OR and AND is mechanical. In Vixie cron and the daemons derived from it, the parser raises a flag when a day field begins with a star. If either flag is raised the two day fields are combined with AND, which is what makes 0 0 * * 5 mean Fridays only. If neither is, they are combined with OR. Note what that implies: */2 in the day of month field still begins with a star, so it still counts as one for this test. The builder implements that rule exactly, and one of the nine presets is the OR trap itself, so you can watch the next ten runs land on dates you never asked for.

    WP-Cron has no timer

    System cron is a daemon. It wakes every minute, compares the clock to every line in every crontab, and runs what matches. Nothing has to happen for it to fire. WP-Cron is not that, and the name is the whole problem.

    In wp-includes/default-filters.php, WordPress hooks its scheduler onto init. On a request it reads the stored list of events out of the options table, and if anything is due it calls spawn_cron(), which sets a doing_cron transient as a lock and fires a loopback POST at wp-cron.php with a timeout of 0.01 seconds and blocking switched off. The request that triggered it does not wait for the result. The lock lasts WP_CRON_LOCK_TIMEOUT seconds, which defaults to MINUTE_IN_SECONDS, so a busy site does not spawn one run per visitor.

    Flow diagram of WP-Cron: a page request arrives, init runs the scheduler, the doing_cron lock is checked for 60 seconds, a non-blocking loopback POST goes to wp-cron.php, due callbacks run late, and a dead branch shows that with no request nothing runs at all. Below it, the four built-in WordPress recurrences in seconds: hourly 3600, twicedaily 43200, daily 86400, weekly 604800.

    Every step in that sequence starts with a page request, and the failure mode falls straight out of it. No request, no scheduler. A quiet site does not run its jobs late, it runs none of them, and when a request finally arrives at 08:40 it runs everything due since midnight at once, inside somebody’s page load. That is the mechanism behind scheduled posts that never publish, and the full account of why WP-Cron is not a cron job is worth reading if publishing on time matters.

    The other half of the cost lands on visitors. The loopback POST is a second HTTP request to your own server, opened during somebody else’s page view. When it is slow, blocked by a firewall or resolving to the wrong address, it shows up as time to first byte, a standard suspect in a diagnostic order for a slow WordPress site.

    The two step fix

    Step one: stop WordPress spawning its own runs. One constant in wp-config.php, above the line that tells you to stop editing.

    define( 'DISABLE_WP_CRON', true );

    The constant is checked before the spawn, so the loopback never happens. It does not disable wp-cron.php itself: that file still runs when requested directly, which is precisely what step two does. If you are unsure what else belongs in that file, the constants that earn their place in wp-config.php is a shorter list than the one circulating on forums.

    Step two: a real crontab entry, on a real timer.

    # every five minutes, run whatever WordPress says is due
    */5 * * * * cd /var/www/example.com && wp cron event run --due-now >/dev/null 2>&1
    
    # same job without WP-CLI installed
    */5 * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

    The WP-CLI line is better where you have shell access: it runs in PHP CLI, it is not bound by the web server’s timeout, and it reports what it ran. The curl line works anywhere and is what most hosting panels give you a box for. Both are what the builder writes into the crontab panel next to the constant, each with its own copy button.

    Intervals, and the four WordPress already knows

    Here is the mismatch nobody warns you about. wp_schedule_event() does not take an expression. It takes a first timestamp, the name of a recurrence, and a hook. The recurrence is a label pointing at a number of seconds, and wp_get_schedules() in wp-includes/cron.php ships exactly four: hourly at 3600 seconds, twicedaily at 43200, daily at 86400 and weekly at 604800, the last of which only arrived in WordPress 5.4.

    Anything else you add yourself through the cron_schedules filter. The builder writes that filter whenever the interval it worked out is not one of the four, along with the guard that stops you scheduling the same event on every page load. Whatever you type into the hook name field is sanitised into a valid PHP identifier first, so the generated code compiles.

    add_filter( 'cron_schedules', function ( $schedules ) {
        $schedules['every_five_minutes'] = array(
            'interval' => 300,
            'display'  => 'Every five minutes',
        );
        return $schedules;
    } );
    
    add_action( 'init', function () {
        if ( ! wp_next_scheduled( 'wpie_media_sweep' ) ) {
            wp_schedule_event( time(), 'every_five_minutes', 'wpie_media_sweep' );
        }
    } );
    
    add_action( 'wpie_media_sweep', 'wpie_run_media_sweep' );

    Two things about that filter. It has to run on every request, not once at activation, because WordPress looks the recurrence up again each time it reschedules: remove the filter and the event fires once more, then stops repeating. And it belongs in a small plugin rather than the theme, since a theme switch takes the schedule with it. If functions.php is where you put everything, this is where that habit bites.

    The deeper mismatch is that most cron expressions are not intervals. 0 9 * * 1-5 is nine in the morning on weekdays: the gap is 24 hours four times, then 72 hours over the weekend. No number of seconds describes it. That is why the builder’s interval analysis reads the expression in UTC and looks 61 runs ahead rather than the ten it displays. A fixed interval is a property of the expression, not of your daylight saving, and ten runs of an office hours schedule sampled mid afternoon otherwise look like a tidy fifteen minutes. Two runs eight years apart are not evidence of an interval either.

    When the gaps are not constant, the generated code takes the guarded route: the event repeats on the greatest common divisor of the gaps, and the callback checks month, day and hour before doing any work. It deliberately does not check the minute. WP-Cron fires on a page request and is regularly late, so a callback that insists on the exact minute is a callback that never runs.

    add_action( 'wpie_office_hours_job', function () {
        $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
    
        if ( (int) $now->format( 'N' ) > 5 ) {
            return; // Saturday or Sunday
        }
        if ( 9 !== (int) $now->format( 'G' ) ) {
            return; // not the nine o'clock hour
        }
    
        // the actual work goes here
    } );

    Every minute is how you get a letter from your host

    * * * * * is 1,440 runs a day and about 43,800 a month. On a system cron calling WP-CLI, every one of those is a full WordPress bootstrap: core loaded, every active plugin loaded, the options table read. On shared hosting priced by CPU seconds or capped by a process count, a minutely job doing nothing at all can be the largest consumer on the account, and the first you hear of it is an email about resource usage.

    Cron also does not wait. If a run takes ninety seconds on a minutely schedule, the runs overlap, stack, and compete for the same database rows. On a real crontab, flock is the one line fix.

    * * * * * flock -n /tmp/wpie-sweep.lock /usr/local/bin/wp cron event run --due-now

    Sensible floors: five minutes for a due-now runner, fifteen for anything that touches image files, hourly or daily for maintenance such as a sweep that finds unused attachments in the media library. The builder flags a minutely schedule on the problems list for exactly this reason.

    What the next ten runs cost to compute

    The run list is not a lookup, it is a search. The builder walks the calendar field by field, testing candidate minutes against the parsed sets, bounded to eight years ahead and forty thousand steps. That bound turns a broken expression into an answer rather than a frozen tab: 0 0 30 2 *, the 30th of February, reports that it can never fire. It is one of the presets.

    Daylight saving is handled by hand. A wall clock time inside a spring forward jump does not exist, so the search nudges it forward and keeps moving in one direction, and an hour repeated in autumn is listed once rather than twice. If a change lands inside the ten runs on show, the problems list says so, because that is the one week a year a nightly job at 02:30 behaves oddly and nobody thinks to blame the clock.

    The problems list is worth reading before you copy anything. It covers both day fields set, a schedule that can never fire, a minutely schedule on shared hosting, 7 used for Sunday, leap year only dates, the non-portable 5/10 form, a step wider than its range, a daylight saving change inside the run list, and WP-Cron’s drift. If a field cannot be parsed, the last readable expression stays on screen and the tool names the field that broke.

    Where the builder stops

    Seconds, years and the Quartz extensions are not covered. ?, L, W and # are refused by name rather than misread, because dropping the character silently leaves a plausible looking expression that fires on the wrong days. Six field expressions with a leading seconds column are refused for the same reason, as are backwards ranges such as 50-10.

    Timezones stop at two, your local clock and UTC. A real crontab file can carry its own CRON_TZ= or TZ= line at the top, and every entry below it is read in that zone. The builder does not model that, so if your crontab has one of those lines, read the run list as being in that zone rather than the server’s.

    The short version

    The five fields are minute, hour, day of month, month, day of week, and the only rule you cannot derive by inspection is that the two day fields are an OR whenever neither begins with a star. Everything else is lists, ranges, steps counted from the start of a range, and a few names. Once you have seen the next ten runs printed out, the expression stops being a guess.

    The WordPress half is a separate and more consequential decision. WP-Cron works because a busy site generates its own heartbeat, and it fails on precisely the sites that most need scheduling to be reliable: the quiet ones, the staging copies, the client site that gets forty visits a day. A constant in wp-config.php and a crontab entry every five minutes move it onto a real timer and take the loopback request off your visitors’ page loads.

    Then keep the schedule as slow as the job allows. Almost nothing on a WordPress site needs to run every minute, most maintenance is happy at hourly, and the difference between those two choices is 1,440 bootstraps a day against 24. Cron is a blunt instrument with a very long memory: it will do exactly what the five fields say, every day, until somebody notices.

    Cron Expression Generator: The Five Fields and the OR Rule

    Table of Contents

    Learn it by building something

    Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

    One mail a week, and then it ends.
    Unsubscribe in one click.

    Photo Editing

    Perspective Correction: Four Corners and One Matrix Put It Straight

    A photo taken at an angle is a projective map, not an affine one, which is why cropping and rotating cannot repair it. Four corners supply the eight numbers that can, and the geometry of the vanishing points can even hand back the object's true aspect ratio.

    SEO & Structured Data

    Redirect Rule Generator: The Rules That Never Fire

    A redirect list rarely breaks. It accumulates: a broad prefix rule that kills every rule below it, a target missing a trailing slash that doubles your hop count, a 301 where a 308 was needed. Generate the Apache, nginx and CSV versions of your rules, then walk a URL through them and watch where it actually goes.

    Troubleshooting

    Gutenberg Block Invalid Content: Validate and Repair Block Markup

    The editor says a block contains unexpected or invalid content and stops there: no line, no attribute, no cause. Here is what it compared, why block markup drifts after a migration, and how to find the exact character that broke it.

    Developer Tools

    Encoding Forensics: How to Fix Mojibake Like ü

    ü is not corruption. It is one umlaut, two UTF-8 bytes, read by a program that believed they were Windows-1252, then saved again so the wrong reading became the content. Here is how to tell which wrong turn your text took, why copied replacement lists only half work, and what to check in the database before you clean anything.

    Design Fundamentals

    Building a Colour Palette From One Decision

    Five swatches picked separately will fight each other. One hue plus arithmetic will not. A color palette generator that runs entirely in your browser, and the HSL and contrast maths that sit behind it.

    Security & Privacy

    Stopping WordPress Hotlinking Without Breaking Image Search

    Hotlinking is another site's HTML pointing at your image URL, so your server pays for their page. Prove it from the access log before you block anything, then write a referrer rule at the web server level that still lets crawlers, CDNs and link previews through.

    Download the free WunderPaint Plugin for WordPress

    The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

    The Image Editor & Design Studio

    Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

    Free

    Chaos Art

    Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

    Pro

    Particle Strokes

    Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

    Pro

    City Diorama

    Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

    Free

    Papercut Art

    Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

    Pro

    3D Earth Studio

    A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.