Operating a site: caches, cronjobs, backups and repairs
This guide is for the person who keeps an Exponential site running, day to day. It takes you through the routine work of keeping it healthy: looking at the caches and clearing the right one, running cronjobs, switching on the static cache, warming the site, taking a backup, knowing which database you run on, reading the logs, and repairing a site that will not start. Follow it top to bottom. Each part takes a few minutes and ends in something you can check.
Warning
.htaccess slows down every request. When .htaccess files are enabled (AllowOverride anything but None), let alone
used, Apache looks for them and reads and evaluates them on every request, in every directory of the path, and
page loading times go up drastically (measured). The only real fix is to switch the feature off for
the virtual host: AllowOverride None in the HTTPS virtual host, and the rules in the virtual host's own
configuration as mod_rewrite rules. That is far faster in production speed tests and well worth the time it takes
to optimize a production configuration. Switch over right away: copy the rules of the
example HTTPS virtual host into your virtual host (with the static cache, also
staticcache_offload.conf), set AllowOverride None, run apachectl configtest, reload Apache and
test the site. The same rules for other servers: nginx, LiteSpeed,
Caddy, IIS, Velocity (built in); where virtual host rules go in a control
panel: Plesk, cPanel, DirectAdmin, ISPConfig.
On LiteSpeed or OpenLiteSpeed, the server's own page cache can also answer anonymous visitors and crawlers without PHP: Page caching with LiteSpeed and OpenLiteSpeed.
Every command is run from the installation root, the directory that contains index.php. When you are logged in as root, add --allow-root-user to the PHP scripts; the scripts refuse to run as root otherwise. In the examples the flag is written out so you can copy them as they are.
Where a topic needs more than a few minutes, the part ends with links to the feature, specification and upgrade pages.
1. Look before you clear: what caches exist
Exponential has about twenty-seven caches: INI, templates, content, translations, image aliases, the HTTP cache, the SQL query cache, and, if you run the Velocity server, its response cache. One command lists them and says how each is cleared.
php bin/php/cache.php list --sizes --allow-root-userExpected output: one block per cache, for example
content on content expiry timestamp content-view-cache set, directory moved aside
var/site/cache/content 3 files, 31.5 KB
template on template directory removed
var/site/cache/template/compiled 227 files, 5.4 MBThe word after the cache name is its tag; one tag clears every cache that carries it. For a one-screen summary of the caches that sit in front of PHP (static, HTTP, query, Velocity, OPcache, APCu):
php bin/php/cache.php status --allow-root-userExpected output ends with PASS status read and has one line each for static, httpcache, querycache, velocity, precompress and php.
In the administration interface the same caches are under Setup > Cache.
2. Clear the right cache
Clear the smallest thing that fixes the problem. Each of these first, with --dry-run, shows what it would do and changes nothing:
php bin/php/cache.php clear --all --dry-run --allow-root-userThen clear for real, choosing by what you changed:
| You changed | Command |
|---|---|
A setting (.ini file) | php bin/php/ezcache.php --clear-tag=ini --allow-root-user |
A template (.tpl) | php bin/php/ezcache.php --clear-tag=template --allow-root-user |
| Content looks stale | php bin/php/ezcache.php --clear-tag=content --allow-root-user |
| A new template or extension was added | php bin/php/ezcache.php --clear-id=template-override --allow-root-user and --clear-id=design_base |
| Everything, when unsure | php bin/php/ezcache.php --clear-all --allow-root-user |
ezcache.php --list-tags and --list-ids print the valid names. --purge physically removes entries instead of only marking them expired, which saves disk space; --expiry="-2 days" with --purge removes only old entries.
The same actions exist per group in exp:cache, which also reports PASS or FAIL and has a --json output for monitoring:
php bin/php/cache.php clear --tag=ini --allow-root-user
php bin/php/cache.php imagealias clear --allow-root-user
php bin/php/cache.php opcache status --allow-root-userLarge caches are moved aside and deleted afterwards, so a clear does not make requests wait; see Cache clears that move directories aside.
The caches in front of PHP
Content is cached again after a clear as soon as the next request renders it. Three caches sit in front of the page rendering and have their own commands:
php bin/php/cache.php httpcache status --allow-root-user # the HTTP cache: entries, size, hits and misses
php bin/php/cache.php httpcache clear --allow-root-user # every stored page becomes stale
php bin/php/cache.php velocity status --allow-root-user # Velocity's response cache
php bin/php/cache.php velocity clear --allow-root-userExpected output of the status commands ends in PASS and a line such as the HTTP cache is on: 4651 entries, generation 22850. Clear the HTTP and response caches after PHP-FPM or Velocity runs the new code, otherwise a page rendered by the old code in between is stored again. If you run Velocity, one command does all steps in the right order:
./console exp:velocity deploy --dry-run --allow-root-userDepth: Velocity response cache, HTTP cache, response cache and navigation, cache console, Velocity engines: deploying a PHP change.
3. Run cronjobs
Cronjobs do the work nobody waits for: publishing scheduled content, importing feeds, cleaning baskets, sending newsletters, building sitemaps.
See what exists and which script belongs to which part:
php runcronjobs.php --list --allow-root-userExpected output: blocks named CronjobSettings: and CronjobPart-frequent:, CronjobPart-infrequent: and so on, each followed by its script files.
Run one part, or one script of a part:
php runcronjobs.php --siteaccess=site frequent --allow-root-user
php runcronjobs.php --siteaccess=site --script=notification.php --allow-root-userUse the name of your own public siteaccess for --siteaccess. The same list is in the administration at Setup > Cronjobs (/setup/cronjobs), where each part has a card with its schedule, its next and last run, a Run part button and its shell command to copy; the output follows while the job runs, and the page proposes the crontab lines. Show what is installed now:
./console crontab:listTo install the schedule open the crontab for editing (./console crontab:edit) and add a line per part. A common starting point is the frequent part every few minutes and the infrequent part once an hour or a day:
*/5 * * * * cd /path/to/installation && php runcronjobs.php --siteaccess=site frequent --allow-root-user >> var/log/cron-frequent.log 2>&1
17 * * * * cd /path/to/installation && php runcronjobs.php --siteaccess=site infrequent --allow-root-user >> var/log/cron-infrequent.log 2>&1Replace /path/to/installation with the root directory. Every part also has a console alias, for example ./console cron:frequent; ./console list cron shows them all. If you run Velocity, its own scheduler can start these tasks without the system cron.
The notification cronjob part has its own guide: Notifications: running them and fixing problems.
The part mailpreferences (also in infrequent) keeps the e-mail consent log and confirmations within their retention time: E-mail preferences: setting them up and running them.
Depth: Cronjobs console, commands, cronjob parts and views as classes, Content jobs (large removals and copies run in batches and are resumed by cron:contentjobs).
4. Turn on the static cache
The static cache stores public pages as plain files that the web server can send without starting Exponential. First see what it would do:
php bin/php/cache.php static status --allow-root-userExpected output lists the sites that can be generated and ends with PASS and a sentence such as the static cache is not enabled in site.ini [ContentSettings] StaticCache; 3 sites can be generated.
Generate the pages of one site, limited to 500 pages so you can see it work quickly:
php bin/php/makestaticcache.php --site=site --max-pages=500 --max-depth=6 --allow-root-userOmit --site to generate every public siteaccess. Only siteaccesses that need no login are offered, because their pages are the same for everybody. The same is possible in the browser at Setup > Cache, section Static content cache, with a Stop button. Check the result:
php bin/php/cache.php static status --allow-root-userThe files are counted per site in var/site/static. To make the web server answer from them, set StaticCache=enabled in settings/override/site.ini.append.php block [ContentSettings], clear the INI cache (php bin/php/ezcache.php --clear-tag=ini --allow-root-user), and check that the web server rules of your installation are active. Remove the generated files with php bin/php/cache.php static clear --allow-root-user.
Depth: Static cache generator, static cache defaults.
5. Warm the site (preload)
After a clear or a deploy the first visitor pays for the render. Warm the pages yourself:
php bin/php/preload.php --apply --siteaccess=site --allow-root-userIt requests the section pages first, then follows the links of the site, with PHP's curl extension (no wget needed); one preload runs at a time, and every run is listed on Setup > Preload. To warm every published page, which suits a cron entry every few minutes:
php bin/php/warm.php --allow-root-user >> var/log/cache-warm.log 2>&1In the browser use the Preload Sites view from the administration: choose the site, a page limit (default 250) and a link depth (default 3), and watch the pages being fetched. Depth: Preloading caches, Preload Sites view, Site cache preloader, HTTP/2 and cache warming.
6. Back up and restore
Exponential keeps state in two places: the database and the files (var/, settings/override/ and settings/siteaccess/, and your own extensions and designs). A backup is both, taken at the same time.
Maintenance mode makes the pair consistent. It answers every visitor with a 503 page while you work:
php bin/php/maintenance.php on --message="Back soon" --until=30m --allow-root-user
php bin/php/maintenance.php status --allow-root-user # prints: Maintenance is on...Take the files:
tar czf backup-files.tar.gz var/storage var/log settings/override settings/siteaccessTake the database with the tool of your database system. Replace USER and DATABASE with your values. -p makes the client ask for the password; never write the password itself on the command line, where it lands in the shell history.
| Database | Backup | Restore |
|---|---|---|
| MySQL or MariaDB | mysqldump --single-transaction -u USER -p DATABASE > backup.sql | mysql -u USER -p DATABASE < backup.sql |
| PostgreSQL | pg_dump -U USER DATABASE > backup.sql | psql -U USER DATABASE < backup.sql |
| SQLite | sqlite3 var/storage/sqlite3/exponential.db ".backup backup.db" | copy backup.db back with the site in maintenance mode |
exponential.db is the file name exp:install uses by default; use the name of your own database file (ls var/storage/sqlite3/).
For SQLite never copy the file while the site is busy; .backup is safe. The schema alone, as a portable file, is also available through php bin/php/ezsqldumpschema.php --type=mysql --user=USER DATABASE schema.sql --allow-root-user.
Then switch the site back on and clear the caches:
php bin/php/maintenance.php off --allow-root-user
php bin/php/ezcache.php --clear-all --allow-root-userTest a restore on another server at least once; a backup you have never restored is a hope. Depth: Maintenance mode.
7. Know your database driver
Which database runs the site is one setting. Read it:
grep -rn "DatabaseImplementation" settings/override settings/siteaccess/*/site.ini.append.php settings/site.iniExpected output: a line such as DatabaseImplementation=ezmysqli. The values are ezmysqli (MySQL, MariaDB), pgsql or ezpostgresql (PostgreSQL), sqlite3 (one file under var/storage/sqlite3/) and mongodb; Oracle comes with its own extension. The aliases are listed in settings/site.ini block [DatabaseSettings], key ImplementationAlias[].
What to remember:
- SQLite is the quickest way to start, and its transactions queue for the single write lock (
SQLiteTransactionWait, default 25 seconds, kept below Velocity's 30-second request timeoutQ.webserver.requestTimeoutso the driver can report "database is busy" before the request is cut off; raise both together). For many editors publishing at once use MySQL or PostgreSQL. - MySQL and PostgreSQL are the usual production choices.
- Oracle and MongoDB are supported; check the pages below before choosing them.
For developers: a failed statement's error number is the database's own, in $db->errorNumber() and, with
$db->setErrorHandling( eZDB::ERROR_HANDLING_EXCEPTIONS ), as getCode() of the eZDBException thrown, so code
can tell a duplicate key from a missing table without reading the message:
| Driver | Duplicate key | Missing table |
|---|---|---|
| MySQL, MariaDB | 1062 | 1146 |
| PostgreSQL (the SQLSTATE, a string) | '23505' | '42P01' |
| SQLite (the result code) | 19 | 1 |
| Oracle (the ORA number) | 1 | 942 |
MongoDB (errorNumber() only; the driver answers false, it throws nothing) | 11000 | - |
$db = eZDB::instance();
$db->setErrorHandling( eZDB::ERROR_HANDLING_EXCEPTIONS );
try
{
$db->query( "INSERT INTO my_table ( id ) VALUES ( 1 )" );
}
catch ( eZDBException $e )
{
if ( $e->getCode() === 1062 || $e->getCode() === '23505' ) // MySQL, PostgreSQL
{ /* the row is there already */ }
}
$db->setErrorHandling( eZDB::ERROR_HANDLING_STANDARD );The SQLite driver throws inside a transaction; outside one it answers false with errorNumber() set. Upgrade
note: PostgreSQL's errorNumber() was 1 for every error and is the SQLSTATE now; with the exception handling,
MySQL on PHP 8.1 and later throws eZDBException instead of PHP's mysqli_sql_exception (the standard handling
is unchanged). Thanks to Mugo Web, whose LoveStack commit 3d18f47551 carried MySQL's error codes into the
exceptions first.
Numbers in the built-in search index
The built-in search engine stores each word of a text with its value as a number, so that a search can filter on
numbers. A very large number in a text field (a 20-digit order number, 1e30) no longer breaks indexing on MySQL
or PostgreSQL: it is stored as the largest number the column holds, and the object is indexed with all its other
words. Each driver knows the range of its integer columns, $db::MAX_INT and $db::MIN_INT (signed 32-bit for
MySQL, PostgreSQL and Oracle, signed 64-bit for SQLite and MongoDB); $db->clampInteger( $value ) gives a value
within that range, cuts a fraction off as (int) does and gives 0 for anything that is not a number. On for every
installation; nothing to do on an upgrade. With thanks to Mugo Web (LoveStack, commit 3774a54ead).
Related objects are indexed with the object that points to them: the searchable fields of an object chosen in an
object relation or relation list field are found when you search for the object that holds the field. The relation
fields of that related object are left out, so indexing never follows a chain of relations. Exponential has worked
this way since 2018 (see relation indexing); LoveStack's commit 5c089f6250
does the same, so a site coming from LoveStack finds the same search results here.
Depth: SQLite database support, SQLite 3 driver, database drivers 16 to 30 September 2026, SQLite and Oracle driver behaviour, MongoDB support.
8. Read the logs
Where to look first:
| What | Where | How |
|---|---|---|
| PHP and application errors | var/log/error.log, var/log/debug.log | tail -n 50 var/log/error.log |
| PHP deprecations (code that a later PHP version will refuse) | var/log/deprecated.log | sort var/log/deprecated.log | uniq -c | sort -rn | head |
| Cronjob output | var/log/ and, for jobs started from the browser, cronjobs/output.log and cronjobs/error.log in the siteaccess var directory | Setup > Cronjobs, section Output |
| Installer | var/log/kickstart.log (passwords masked) | tail -n 50 var/log/kickstart.log |
| Who did what (logins, content changes, permission changes) | the audit trail | php bin/php/audit.php status --allow-root-user |
The audit trail is a signed, tamper-evident log. Check that it is intact and read the newest events:
php bin/php/audit.php status --allow-root-user
php bin/php/audit.php tail --channel=access --lines=20 --allow-root-user
php bin/php/audit.php verify --allow-root-userExpected output of status: Audit: enabled and one line per channel (content, access, system, ...) ending in intact. To see more in the page itself while you investigate, set DebugOutput=enabled in settings/override/site.ini.append.php block [DebugSettings], reload the page, and switch it off again; never leave it on in production. Depth: Audit trail, audit event model, debug output improvements.
PHP deprecations and log file sizes
PHP 8 reports a deprecation for every use of something a later PHP version removes, often the same one many times
in one page. They have a log of their own, var/log/deprecated.log, with each distinct message written once per
request; they are not kept in memory or listed in the debug output, which only counts them (the depr value of the
debug bar, and the warning count). For a developer the file is the list of
what to fix before the next PHP upgrade; for an administrator a page with debug output on needs less memory.
# settings/override/site.ini.append.php
[DebugSettings]
# enabled (default): deprecations go to deprecated.log; disabled: they are warnings, as before 6.0.15
DeprecatedLog=enabled
# Also write them while debug output is off (production): add the level to the list
AlwaysLog[]=deprecatedDevelopers write one of their own with eZDebug::writeDeprecated( 'Use newThing() instead', __METHOD__ ).
Every log file (error.log, warning.log, deprecated.log, storage.log, the cronjob logs ...) is rotated when it is
larger than site.ini [FileSettings] LogFileMaxSize (default 2097152 bytes, 2 MB) into .1, .2, .3;
LogFileRotateCount (default 3) is how many are kept, 0 turns the rotation off. With the defaults a log takes at
most 8 MB on disk. The constants EZPUBLISH_LOG_MAX_FILE_SIZE, CUSTOM_LOG_MAX_FILE_SIZE and their *_ROTATE_FILES
in config.php still win over these settings; ./console exp:config:list --only-set shows whether config.php sets
them, and ./console exp:config:unset EZPUBLISH_LOG_MAX_FILE_SIZE --apply hands the size back to site.ini (see
Setting up .htaccess and config.php from the console).
Upgrading: both are on after the upgrade. Log files are rotated at 2 MB instead of 200 KB (set
LogFileMaxSize=204800 for the old size), and deprecations no longer appear in warning.log or the debug list
(set DeprecatedLog=disabled for the old behavior). Both build on work by Mugo Web in LoveStack (commits
e39dde30ff, fbbd333a85, e20570a7ad, b7832d110e), with thanks.
Remove expired sessions and old versions regularly if you do not run the matching cron parts:
php bin/php/ezsessiongc.php --allow-root-user
php bin/php/cleanupversions.php --help --allow-root-user9. Repair a site that will not start
The most common cause is a missing vendor/ directory: the site shows a page that explains it. Repair it without a shell session, or with one command:
sudo -u WEB_SERVER_USER php bin/php/exprepair.php --create-keyThe key is printed once and works once. Open any page of the site, paste the key into the field and press Enter; three steps run (install the libraries, regenerate the autoloads, clear the caches) and the page reopens when they are done. Check the state at any time, and switch the feature off when you do not want it:
php bin/php/exprepair.php --status
php bin/php/exprepair.php --disableFor a site that starts but misbehaves, the usual order is: clear the INI and template caches (part 2), check var/log/error.log (part 8), run php bin/php/checkclasses.php --allow-root-user to find a class PHP refuses to load, and regenerate the autoloads after adding or renaming a class:
php bin/php/ezpgenerateautoloads.php -e --allow-root-user
php bin/php/ezcache.php --clear-all --allow-root-userWhen every page fails with a PHP parse error in config.php, the console still starts and says so ("Warning:
config.php does not compile: ..."). Fix the line it names, copy back the last backup from var/backup/config/ or
var/backup/setup/, or start again from the template:
./console exp:setup:status # names the syntax error
./console exp:setup:config --force --apply # replaces config.php with config.php-RECOMMENDED, backup firstDepth: Repair from the browser, repair (upgrade page), Setting up .htaccess and config.php from the console.
10. A weekly checklist
php bin/php/cache.php status --allow-root-userends inPASS.php bin/php/audit.php status --allow-root-usershows every chainintact.tail -n 50 var/log/error.logshows nothing new that you do not understand../console crontab:liststill shows your cron entries.- A backup from the last seven days exists and has been restored at least once on another machine.
Related pages
- Running on the Velocity server: Velocity persistent-worker server and Velocity engines.
- Installing a new site: Installing in one command and Kickstarter.
.htaccessandconfig.php: Setting up .htaccess and config.php from the console.- What changed, month by month: the Velocity chronicle.
- Other guides: Deploying (servers, HTTPS, shipping a change), Security and audit (hardening, roles, the audit trail), Upgrading.
- Words used here: Glossary.
Become an Exponential Partner
Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.