Back to Blog Engineering
2 min read ✉ Subscribe
Scheduled publishing that fires when nobody is watching
Engineering

Scheduled publishing that fires when nobody is watching

If your "publish at" check only runs when an admin loads the dashboard, then a post scheduled for 6 a.m. goes live whenever somebody happens to sign in.

AX
Alien XP
Jul 16, 2026 · 2 min read

Scheduling is one of those features that is easy to build 90% of, and the missing 10% is the entire point. The naive version works like this: when the content screen loads, check for anything whose publish time has passed, and publish it.

That is correct, it is cheap, and it means a post scheduled for six in the morning goes live at 9:40 when you open the panel with your coffee. On a quiet site it might sit unpublished for days.

The check has to run without a request

Which means cron — or a timer, or a queue worker, but on a single server cron is the honest answer:

* * * * * www-data /usr/bin/flock -n /run/lock/publish.lock \
  /usr/bin/php /var/www/app/bin/publish-due.php >/dev/null 2>&1

Three details in that line matter more than they look.

  • flock -n — if a run overlaps the previous one, skip it rather than queue it. Minute-ly jobs that occasionally take ninety seconds will otherwise pile up until the box falls over.
  • The right user — run as the user that owns the content files, or the job will publish successfully and then fail to write, leaving the state inconsistent.
  • Discard the output — a cron job that prints on success emails you every minute forever, and you will stop reading those emails, including the ones that matter.

Keep the on-load check as well

This sounds redundant and is not. Running the same function when the screen loads means the panel never shows something as scheduled when its time is in the past — which is the state that makes people think the feature is broken.

function publish_due($now = null) {
    $now = $now ?: time();
    // move anything due from scheduled -> published
    // return the slugs that changed, so the caller can log them
}

Write it as one idempotent function, call it from both places. A no-op when nothing is due, so calling it on every page load costs nothing.

Publishing is not the only side effect

This is the part people forget. When a post goes live at 6 a.m., everything that depends on "what is published" is now stale:

  • The sitemap does not contain it, so search engines will not be told for however long your regeneration interval is.
  • The RSS feed does not contain it, so subscribers do not get it.
  • Any cached index or listing page still shows the old set.

Do those in the same function that flips the status. If they live in a separate nightly job, then a post published at 6 a.m. is invisible to crawlers and feed readers until midnight — which for a piece of writing whose whole purpose is to be read on the day it lands is the same as not publishing it.

Test it by scheduling something for sixty seconds in the past, running the worker by hand, and checking the public URL — not the admin list. The admin list will happily tell you it worked.
Share this
AX

Alien XP

Engineering · 10 essays

Runs the servers behind AlphaPanel and writes up what breaks.

Keep reading

All posts