Cache warming via Cron: boring, cheap, production-proven
Every night at 02:00, our cache resets.
At 02:01, Google's bots arrive. By 02:03, real users from Asia start coming in — it's morning there.
For the first 20 minutes, TTFB was sitting at 4 seconds. Now it's 0.7. The difference: one bash script and a crontab entry. No Redis Cluster. No Varnish.
Cold cache is a revenue problem
This isn't "a bit slower." It's direct conversion loss.
After a nightly cache reset or a deploy, PHP regenerates every page from scratch — database queries, template compilation, iblock fetches, the whole chain. On a project with 28k SKUs and 200–300 concurrent users at peak, that means TTFB of 3–5 seconds on the first requests to popular pages.
On one of our ecom projects, we measured it: in the first 15 minutes after a cache reset, bounce rate climbed 40% compared to normal night traffic. Users simply left without waiting.
Those are orders missing from the November report.
The fix seemed too simple: warm the cache before users arrive.
How PHP cache warming works
PHP cache warming is the practice of pre-loading page cache by making HTTP requests to your URLs before real users arrive. A script crawls your URLs, PHP generates each page and stores the result in cache. When real users arrive, the cache is already warm and TTFB is normal.
Three approaches. Most projects only need the first one.
Option 1 — wget + sitemap.xml
Fifteen lines of bash. Works on any PHP project.
#!/bin/bash
SITEMAP_URL="https://example.com/sitemap.xml"
DELAY=0.5 # seconds between requests
URLS=$(wget -qO- "$SITEMAP_URL" | grep -oP '(?<=<loc>)[^<]+')
COUNT=0
for URL in $URLS; do
wget -q --spider "$URL" &>/dev/null
COUNT=$((COUNT + 1))
sleep $DELAY
done
echo "$(date): warmed $COUNT URLs" >> /var/log/cache-warm.log
Run it via cron an hour before peak traffic or right after deploy:
0 1 * * * /usr/local/bin/warm-cache.sh
The --spider flag is the key: wget makes the request without downloading the response body. Page gets generated, cache gets filled, no bandwidth wasted. DELAY=0.5 is non-negotiable — remove it and you're running a DoS on yourself.
On a project with 2,000 URLs and 0.5s delay, the warmup takes around 17 minutes. That's fine if you schedule it an hour before peak.
Option 2 — PHP/Bitrix CLI
When you need more control: prioritize high-traffic categories, skip admin pages, log response codes.
Bitrix supports running PHP from the command line through agents. Here's a warm_cache.php script:
<?php
define('STOP_STATISTICS', true);
define('NO_KEEP_STATISTIC', true);
define('NO_AGENT_CHECK', true);
define('NOT_CHECK_PERMISSIONS', true);
$_SERVER['DOCUMENT_ROOT'] = '/var/www/html';
require('/var/www/html/bitrix/modules/main/include/prolog_before.php');
$urls = [
'https://example.com/',
'https://example.com/catalog/',
// add priority pages manually or fetch from DB
];
foreach ($urls as $url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_NOBODY, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
file_put_contents('/var/log/cache-warm.log',
date('Y-m-d H:i:s') . " $url HTTP/$code\n",
FILE_APPEND
);
usleep(500000); // 0.5 seconds
}
Add to crontab:
0 1 * * * php /var/www/html/local/scripts/warm_cache.php
This one lets you prioritize. Homepage, catalog, top 10 categories first — done in 5 minutes. Everything else fills in after.
Option 3 — Next.js ISR for headless setups
If your frontend runs on Next.js with Bitrix as the data source, you've got two cache layers to warm: the PHP cache on the backend and the ISR cache on the frontend. After a Next.js deploy, both can be cold.
Next.js supports a revalidation endpoint you can trigger directly:
curl -X POST "https://example.com/api/revalidate?path=/catalog&secret=YOUR_SECRET"
In practice, we trigger this immediately after deploy via a CI/CD hook. The Bitrix wget warmup runs in parallel. Both caches are warm before the first user hits anything.
How not to take your server down while warming
The first mistake people make: removing the sleep. Seems reasonable — you want to warm 2,000 URLs in 10 minutes, not 17. What you actually get: server under load, slow responses, potentially a PHP-FPM crash.
Throttling is non-negotiable. At least 300ms between requests on a loaded project, a full second on weaker servers. Pick the right value by watching top during a test run on staging — don't guess.
Only one warmup job should run at a time. Use a lock file:
LOCKFILE="/tmp/cache-warm.lock"
if [ -f "$LOCKFILE" ]; then
echo "Already running" >> /var/log/cache-warm.log
exit 0
fi
touch "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT
Always log. >> /var/log/cache-warm.log is not optional. When someone asks next month whether the warmup ran that night, the answer should be in a file. Not in someone's memory, not guesswork. The same principle behind the 13-task cron infrastructure: boring automation beats clever automation.
When cache warming won't help
Cache warming solves one specific problem: slow page generation after a cache reset. If the issue is different, warming won't help.
Patterns that get confused with cold cache:
- TTFB is consistently high throughout the day, not just after a reset. That's N+1 queries or missing indexes. Covered in detail in Bitrix doesn't slow down. The way teams use it does.
- TTFB is fine but the page feels slow in the browser. That's LCP, JS bundle size, CSS blocking — a frontend problem, not a server one.
- Warmup works but cache expires too quickly. That's an aggressive TTL or invalidation strategy. Fix the strategy, not the warmup.
Wrapping up
I've done a lot of things to improve PHP project performance. Few of them took 2 hours and had this kind of measurable impact.
15 lines of bash, one crontab entry. TTFB after cache reset dropped from 4 seconds to 0.7. Night-time bounce rate went back to normal.
It doesn't scale to a large marketplace with a million SKUs — there you need different tools. But on a typical Bitrix ecom project with 5k–50k SKUs, this is all you need.
Boring. Cheap. It works.