— Article — № 132

132 —Joomla

Joomla 4 500 after Akeeba restore: the missing action_logs fix

A routine Akeeba restore. A blank white 500. Forty minutes of grep, schema reconstruction and one quiet INSERT later, the Joomla 4 site was back.

Paper schema sheet with CREATE TABLE action_logs, brass plate, wax-sealed RESTORE envelope, manila Akeeba tag on bone linen.
Hero · staged still№ 132

23:41 on a Friday. A Dutch agency we work with had just finished a routine restore of a Joomla 4 site from Akeeba Backup to a fresh staging server. The DB import ran clean. The file extraction completed without warnings. Then they hit the frontend URL and got a blank white page with the single character that ends most Joomla admin weekends: 0.

The site was a Joomla 4.4.3 multi-author news platform, around 12,000 articles, migrating from PHP 8.1 production to PHP 8.2 staging ahead of a php.net version cliff. The Akeeba profile was inherited from a previous developer who had left the project two years earlier. The handover document was a single PDF with the word backups and a tickbox next to it. Nothing else.

This post is the full incident log: the first error line, the diagnosis, the SQL that brought the legacy site back, and the small change to the Akeeba profile that means it does not happen on the next restore. If you run Joomla 4 in production, this is one of the more avoidable Friday-night phone calls.

The first error line

The agency lead opened a Loom and screen-shared into the staging server. Apache's error log had been writing the same line every few seconds since the restore finished:

[Fri Jun 06 23:42:11.218443 2026] [proxy_fcgi:error] [pid 8141]
[client 10.0.0.4:54231] AH01071: Got error 'PHP message:
1146 Table \'staging_jdb.jos_action_logs\' doesn\'t exist'

Joomla's own log under administrator/logs/error.php was more verbose but said the same thing:

PHP Fatal error: Uncaught Joomla\Database\Exception\ExecutionFailureException:
Table 'staging_jdb.jos_action_logs' doesn't exist
SQL=INSERT INTO `jos_action_logs` (`message_language_key`, `message`,
`log_date`, `extension`, `user_id`, `item_id`, `ip_address`) VALUES ...

One missing table. Four columns of stack trace. Total site outage on every URL, including /administrator. The reason is not subtle once you see it: the actionlog system plugin ships enabled in every Joomla 4 install, it listens to events like onUserLogin, onContentAfterSave, and onExtensionAfterInstall, and on Joomla 4 even an unauthenticated frontend hit eventually touches a code path that wants to write a log line. If the write fails, the request 500s. If every request 500s, the site is dark.

To confirm there was nothing else missing, the first thing we ran was a one-liner over the day's error log:

grep -i "doesn't exist" /var/log/apache2/error.log \
  | grep -oE "'[^']+'" \
  | sort -u

The output was four lines, all in the same family:

'staging_jdb.jos_action_logs'
'staging_jdb.jos_action_logs_config'
'staging_jdb.jos_action_logs_extensions'
'staging_jdb.jos_action_logs_users'

Nothing else. The rest of the schema, all 184 other tables, had imported cleanly. The blast radius was exactly the User Actions Log component, and the User Actions Log component was bringing the site down.

What Akeeba had quietly excluded

The Akeeba Backup engine has a tab in every profile called Database table filters. It accepts two kinds of rule: skip the table's data (export the schema, drop the rows) and skip the table entirely (no schema, no rows). Both are powerful on a live, growing site. The #__action_logs table in particular grows fast on busy installs. Every login, every article save, every plugin toggle is one INSERT. On a 12,000-article news site with eight editors, it had reached 412MB on production. Nightly archives were running 14 minutes longer than they should have, and someone, at some point, had added the action-logs family to the skip-entirely list to bring backup time down.

That decision was reasonable for incremental rolling backups. The developer who made it almost certainly knew that fresh installs of Joomla create these tables from installation/sql/mysql/base.sql at install time and that the tables would be present on any new environment. What they did not account for is that an Akeeba restore does not run the Joomla installer. It restores exactly what is in the archive. If the archive has no schema for those tables, the restored database has no schema for those tables, and every request that tries to log an action hits a missing-table exception.

The Joomla documentation covers the component but says nothing about the schema dependency of the core plugin on those tables. Akeeba's own docs on database filters warn you to be careful, but a warning in a configuration tab is invisible 18 months later when a different person clicks Restore.

The 40-minute fix

The full repair took 40 minutes. The bulk of it was finding the canonical CREATE TABLE statements for a fresh Joomla 4.4.3 install. The fastest path is to download the matching Joomla release ZIP, unpack it locally, and open installation/sql/mysql/base.sql. Search for action_logs and copy the four CREATE TABLE blocks verbatim. They are stable across the entire Joomla 4.x line, but always match the version of the running site to be safe.

Replace the #__ placeholder with your actual prefix (here, jos_), then run the lot through the staging database. The agency's full repair script looked like this, with the noisy bits trimmed:

CREATE TABLE IF NOT EXISTS `jos_action_logs` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `message_language_key` varchar(255) NOT NULL DEFAULT '',
  `message` text NOT NULL,
  `log_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `extension` varchar(50) NOT NULL DEFAULT '',
  `user_id` int NOT NULL DEFAULT 0,
  `item_id` int unsigned NOT NULL DEFAULT 0,
  `ip_address` varchar(40) NOT NULL DEFAULT '0.0.0.0',
  PRIMARY KEY (`id`),
  KEY `idx_user_id_logdate` (`user_id`,`log_date`),
  KEY `idx_user_id_extension` (`user_id`,`extension`),
  KEY `idx_extension` (`extension`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `jos_action_logs_extensions` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `extension` varchar(50) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `jos_action_logs_users` (
  `user_id` int unsigned NOT NULL,
  `notify` tinyint unsigned NOT NULL,
  `extensions` text NOT NULL,
  PRIMARY KEY (`user_id`),
  KEY `idx_notify` (`notify`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `jos_action_logs_config` (
  `id` tinyint unsigned NOT NULL AUTO_INCREMENT,
  `type_title` varchar(255) NOT NULL DEFAULT '',
  `type_alias` varchar(255) NOT NULL DEFAULT '',
  `id_holder` varchar(255) DEFAULT NULL,
  `title_holder` varchar(255) DEFAULT NULL,
  `table_name` varchar(255) DEFAULT NULL,
  `text_prefix` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

The schema alone is enough to stop the 500s. To stop the User Actions Log admin view from itself throwing on an empty extensions list, also seed jos_action_logs_extensions with the default rows Joomla expects:

INSERT INTO `jos_action_logs_extensions` (`extension`) VALUES
('com_banners'), ('com_cache'), ('com_categories'),
('com_config'), ('com_contact'), ('com_content'),
('com_installer'), ('com_media'), ('com_menus'),
('com_messages'), ('com_modules'), ('com_newsfeeds'),
('com_plugins'), ('com_redirect'), ('com_tags'),
('com_templates'), ('com_users'), ('com_checkin'),
('com_scheduler'), ('com_workflow');

The site came back on the first reload. Backend login worked. The Action Logs admin view loaded clean and empty, which is the correct state after a fresh schema with no historical events.

The 40-minute clock, broken out

For anyone working this kind of incident for the first time, here is roughly how the time spent:

  1. 0 to 5 minutes. Tail error.log. Confirm there is one error class repeating. Confirm the missing object is a table, not a column, file, or class. The cheap one-liner above gives a complete list.
  2. 5 to 15 minutes. Identify the Joomla release in use (cat administrator/manifests/files/joomla.xml | grep version). Download the matching release ZIP from the official archive. Unpack locally. Open installation/sql/mysql/base.sql in your editor.
  3. 15 to 30 minutes. Extract the relevant CREATE TABLE statements, replace the #__ prefix with the live prefix, and run them. Tools like the mysql client are fine for this; a GUI is fine too. Run the seed INSERTs.
  4. 30 to 40 minutes. Reload the site, log in, walk the most-trafficked URLs, check the error log is silent. Verify the admin User Actions Log component loads. Take a fresh Akeeba archive of the now-correct DB as your new baseline.

The single longest sub-task is almost always finding the right CREATE TABLE statements for the right Joomla version. If you maintain more than two Joomla 4 sites, keep a folder somewhere with the unpacked installation/sql/mysql/ tree from every minor release you support. That alone shaves 10 minutes off any future incident.

Audit the restore, not just the backup

The change the agency made the following Monday took 90 seconds. They opened the Akeeba profile, moved the action-logs filters out of the nightly profile and into a new nightly-fast profile, and created a weekly-full profile with no exclusions at all. They documented both profiles in a one-page README in the repo. They wrote a Sunday cron that does a dry-run restore of the weekly-full archive to a scratch database and runs SHOW TABLES against a reference list of expected Joomla 4 tables. Any diff sends an email.

That last step is the one most teams skip and the one that pays back the fastest. A backup that you have never restored is not a backup. It is a file that exists.

When we built Pier we ran into this exact thing on a customer's legacy site, twice within a month on two different Joomla projects. The way we ended up handling it: the built-in MySQL editor runs a schema diff between the connected database and a reference Joomla 4 schema bundled with the app, so missing tables flag themselves in the sidebar before you ever push code that depends on them. The full version history of every SQL change you run is kept locally, so the repair script above would have been one click to replay on the next site.

The smallest thing you can do today: open your active Akeeba profile, click the Database table filters tab, and screenshot it. If #__action_logs, #__action_logs_extensions, #__action_logs_users or #__action_logs_config appear anywhere in that list, you have a restore-night problem waiting. Move them to a faster nightly profile, keep a weekly profile without exclusions, and sleep better on Fridays.

— Questions —

Will Joomla recreate the missing action_logs tables on its own?

No. Joomla only writes core schema during a fresh install or an update run. A restore replays the archive verbatim, so any table excluded by the backup filter stays missing until you create it.

Is it safe to exclude action_logs from nightly backups?

Yes, as long as at least one weekly profile includes the full action_logs family. Treat the fast profile as routine ops and the full profile as your disaster-recovery baseline.

What if my table prefix is not jos_?

Replace jos_ with your actual prefix in every CREATE TABLE statement. The prefix is in configuration.php as the public $dbprefix value, or under #__ in the original Joomla base.sql.

Does the same problem hit Joomla 3 sites?

The action_logs tables landed in Joomla 3.9, so the same Akeeba exclusion pattern can produce the same outage on 3.9 and 3.10 installs after a full restore.