turnley.dev

PHP and MySQL code for faucet operators

Tools & Calculators

OPS-003

Run a cron job on hosting that has no cron

A key-gated HTTP entry point plus a lock file, so an external pinger can drive your scheduled work without letting anyone else drive it.

Shared hosting either has no cron, has one that runs at intervals you cannot choose, or has one you cannot see the output of. Meanwhile the site needs periodic work: pruning tables, re-verifying sites, retrying failed payouts.

Making the job reachable over HTTP solves it, and creates two problems that have to be solved with it. Anyone who finds the URL can trigger the job as fast as they can request it, so it needs a secret. And a slow job triggered again before it finishes will run twice concurrently, so it needs a lock.

A lock file with a non-blocking flock handles the second in three lines and releases automatically if the script dies.

PHP
<?php
// cron.php — CLI, or HTTP with ?key=SECRET
require __DIR__ . '/includes/bootstrap.php';

$cli = PHP_SAPI === 'cli';
if (!$cli) {
    header('Content-Type: text/plain');
    if (!hash_equals((string) setting('cron_key'), (string) ($_GET['key'] ?? ''))) {
        http_response_code(403);
        exit("no\n");
    }
}

// Non-blocking lock: a second run exits instead of piling up.
$lock = fopen(sys_get_temp_dir() . '/app_cron.lock', 'c');
if ($lock === false || !flock($lock, LOCK_EX | LOCK_NB)) {
    exit("already running\n");
}

set_time_limit(0);
ignore_user_abort(true);          // keep going if the pinger hangs up

$started = microtime(true);

prune_old_rows($pdo);
reverify_stale_sites($pdo);
retry_failed_payouts($pdo);

setting_set('cron_last_run', date('Y-m-d H:i:s'));

printf("done in %.1fs\n", microtime(true) - $started);
flock($lock, LOCK_UN);
fclose($lock);

Using it

If real cron exists, use it and skip the key:

*/15 * * * * php /home/user/public_html/cron.php

If it does not, point any external monitoring service at the URL with the key. A five minute uptime check doubles as a scheduler.

Record the last run time in settings and show it in admin. A cron that silently stopped is otherwise invisible until something else breaks.

What bites people

Disallow cron.php in robots.txt and keep the key out of any link. A crawler that finds the URL will run your job on its own schedule.

Do not do everything in one pass on a large table. Prune in batches with a LIMIT so a long-running delete cannot hold locks for the whole run.

ignore_user_abort matters: without it, the pinger timing out kills the job halfway through.

Also in Admin and Operations