How to set up a cron job to auto-delete expired blog posts

Web publishers everywhere deal with content that has a shelf life. Promotional announcements, seasonal campaigns, and event coverage all become irrelevant after a date passes. In Australia, where councils in Brisbane and the City of Sydney publish grant windows and event listings that expire after a few weeks, publishers often leave stale posts online long after they have outlived their usefulness. An automated cleanup routine saves hours of manual work.

A cron job is a scheduled task running on your web server. Combined with a small script that targets records in your posts table, you can automatically remove anything whose expiry timestamp has passed. The approach works on self-hosted WordPress, Laravel apps, or custom Node.js blogs. It also helps Australian publishers stay aligned with the Australian Privacy Principles under the Privacy Act 1988.

The walkthrough below covers the script itself, the cron expression, and the safeguards you should put in place before letting automation touch your database. The steps assume readers are comfortable with command-line access and basic scripting.

Understanding the expiry pattern your CMS already tracks

Most content management systems store a publish date and an optional end date. WordPress uses a custom field, while Rails applications store an expires_at column on the posts table. Add this field through a database migration, treat it as a DATETIME, and backfill existing rows with NULL so the routine ignores permanent posts.

Before writing any code, confirm the basics of your schema:

This short list catches most schema drift on legacy sites.

In Australia, timezone handling deserves extra attention. AEST is UTC+10 and AEDT during daylight saving is UTC+11. Servers hosted overseas will shift your expiry calculations by several hours. Store every timestamp in UTC and convert to the reader's local timezone only when rendering the page.

The cleanup logic itself is straightforward. The script issues a single SQL statement targeting rows where expires_at IS NOT NULL and expires_at <= NOW(). Hard delete the row or move it to an archive table. The Notifiable Data Breaches scheme under the Privacy Act requires careful handling of personal information, even in archived form.

Building the deletion script

The simplest approach is a small script in the language your stack already uses. A PHP script on a LAMP server, a Node.js script on a modern deployment, or a Python script on a Linux box all work. The important parts are the database connection, the deletion statement, and a logging block that records each removal.

Place the script outside the web root, such as /usr/local/bin/expire-posts.sh, and make it executable with chmod +x. Avoid putting credentials directly in the script; use a restricted database account whose only privilege is DELETE on the posts table. Run a soft-delete approach by issuing UPDATE posts SET status = 'archived' WHERE ... instead of a hard DELETE. This keeps the row available for an admin to recover if a customer calls before noon AEDT complaining that a campaign page vanished too early.

Configuring the cron schedule

A cron schedule is a line in your crontab telling the system when to run the command. Open your crontab with crontab -e and append a line at the bottom. The five fields represent minute, hour, day of month, month, and day of week. Running the cleanup every hour looks like 0 * * * * /usr/local/bin/expire-posts.sh >> /var/log/expire-posts.log 2>&1. The trailing 2>&1 redirects errors into the same log.

The frequency you choose depends on how time-sensitive your posts are. A news outlet in Perth might run the cleanup every fifteen minutes during business hours, while a slow-paced travel blog could get away with once a day at 3am. Whatever you pick, make sure your server timezone matches your expectation by running date in the shell.

Cron jobs can also be added through hosting control panels such as cPanel, Plesk, or RunCloud. These translate your selections into the underlying syntax and let you specify an email for failure notifications. For e-commerce sites that rely on timed offers, some Australian operators set up a monitoring alert in Sydney through a service like Better Uptime so the team knows within minutes if the script stops.

Testing the routine safely

Before pointing cron at the real script, run it manually a few times. Set up a staging database that mirrors production, populate it with posts that have expired timestamps, and confirm the expected outcome. Verify that the script logs the correct count, that errors are captured, and that nothing else in the database has been touched. A simple SELECT COUNT(*) FROM posts WHERE expires_at <= NOW() before and after the run gives quick confirmation.

Edge cases deserve explicit testing. What happens when expires_at is NULL on a long-lived cornerstone article? The cleanup should skip it. What happens when the database is briefly unreachable? The script should log the failure and exit with a non-zero status. Some Brisbane developers use a wrapper script that retries the connection twice before giving up, a sensible pattern for shared hosting.

It is also worth running a dry-mode test. Add a --dry-run flag that prints what would be deleted without touching any rows. Keep the flag on during the first week of operation while you build confidence. Once you trust it, drop the flag and let the real cleanup run.

Hard delete versus soft delete and the legal angle

Choosing between a hard delete and a soft delete is more than a technical preference in Australia. The Privacy Act 1988 and the Australian Privacy Principles govern how organisations handle personal information, and the Notifiable Data Breaches scheme can apply when expired posts contain identifiable data that an organisation failed to remove. A hard delete makes sense when guest posts include author names, email addresses, or quoted comments.

If your expired content holds commercial value, a soft delete lets you review and repurpose it. Some Adelaide-based real estate blogs archive expired listings for six months before final removal. Others in Hobart use archived posts to power a year-in-review feature each December. Both approaches are valid; pick the one that matches your retention policy.

Approach Speed of removal Recoverability Privacy compliance Best suited for
Hard delete Immediate None Strongest Personal data, expired promotions
Soft delete Immediate in view Full until deletion Requires later purge Repurposable content, archives
Move to archive table One-step relocation Full until purge Strong Year-in-review, audit trails
Flag and review Deferred Full Manual compliance High-value commercial posts

For most Australian small businesses, soft delete followed by a quarterly purge strikes the right balance. It satisfies the spirit of the Privacy Act while keeping your editorial team in control of the final disposal.

Troubleshooting common cron issues

Even with a tested script, cron jobs sometimes misbehave. The classic symptom is that the script runs perfectly when invoked by hand but does nothing under cron. This usually comes down to environment differences. Cron runs with a stripped-down environment, so a missing PATH, an unset database variable, or a different working directory will silently break things. The fix is to set the variables explicitly at the top of the crontab.

Another common issue is permissions. If the script cannot read its credentials file or write to its log file, it will exit with an error you only see by tailing /var/log/syslog or /var/log/cron. Run ls -l on the script and the log file to confirm ownership. A common pattern is to run the cron entry as the same user that owns the web application files, often www-data on Debian-based servers.

If your hosting provider throttles cron to once per five or fifteen minutes, you may need a workaround. Some Australian managed-hosting customers on platforms like VentraIP or Panthur have reported limits that prevent sub-five-minute schedules. In those cases, consider switching to a serverless scheduler such as AWS EventBridge or a GitHub Action that calls a protected endpoint. Whatever you choose, make sure the cleanup logic stays idempotent so overlapping runs cannot damage your archive. Before you go further with payments and monetisation, take a moment to read about handling Flutterwave payments, since many businesses combine cleanup routines with revenue workflows.

Everyday habits that shape your schedule

A few practical Australian habits should shape your cleanup schedule. Most office workers in Sydney and Melbourne start their day around 9am local time, which means a cleanup that runs at 8:55am AEST will catch the freshest batch of overnight-expiring posts before the team logs on. Scheduling a heavy database operation during the AFL or NRL broadcast window risks slowing the site when readership is at its peak.

If you operate a café review blog in Fitzroy or a tourism guide in Cairns, expect seasonal swings. Australian summer brings a flood of festival posts that all expire within weeks. Plan for this by rotating your log with logrotate and giving the database account enough privilege to handle the volume.

A short pre-launch checklist:

Once the cleanup runs smoothly, readers in Brisbane, Perth, and Canberra see only fresh posts, search engines stop indexing stale pages, and your team spends less time on admin. Browse the scripts in our store, subscribe to the newsletter, and reach out via the community forum whenever you need a second opinion on a tricky cron schedule.