Note: This issue was authored with AI assistance (Claude Code) and has not yet been reviewed by a human. Please review the details before acting on it.
On every request — including front-end pageviews where no Stream UI is rendered — Stream issues two duplicate queries against the Action Scheduler table to check whether the async deletion is running.
Sample (table prefix anonymised):
SELECT a.action_id FROM wp_actionscheduler_actions a
WHERE 1=1 AND a.hook='stream_erase_large_records_action'
AND a.status IN ('in-progress', 'pending')
LIMIT 0, 1
Backtrace (abridged):
do_action('init')
→ WP_Stream\Plugin::init (classes/class-plugin.php:252)
→ new WP_Stream\Settings (classes/class-settings.php:59)
→ Settings::get_options (classes/class-settings.php:554)
→ Settings::get_defaults (classes/class-settings.php:580)
→ Settings::get_fields (classes/class-settings.php:299)
├── Admin::is_running_async_deletion (classes/class-settings.php:361) ← query #1
└── Settings::get_deletion_warning (classes/class-settings.php:370)
└── Admin::is_running_async_deletion (classes/class-settings.php:602) ← query #2
Root cause
Settings::__construct eagerly populates $this->options = $this->get_options() on the init hook (priority 9). get_options() needs defaults, get_defaults() derives them by walking get_fields(), and get_fields() happens to embed two pieces of dynamic UI state — the type and the desc of the delete_all_records field — both of which call Admin::is_running_async_deletion() (classes/class-admin.php:716), which in turn calls as_has_scheduled_action() with no caching.
The check is purely admin UI state: its only two callers decide whether to render a "deletion in progress" warning on the Stream settings screen. Computing it on front-end pageloads is unnecessary, and computing it twice in the same admin pageload is redundant.
Impact
Two DB hits on every pageload of every site that has Stream active, regardless of where the request is heading. Individually cheap (~0.5 ms each, indexed query), but the queries serve no purpose outside the Stream settings screen.
Suggested fix
Short-circuit Admin::is_running_async_deletion() outside admin context, and memoise per-request:
public static function is_running_async_deletion() {
if ( ! is_admin() ) {
return false;
}
static $cached = null;
return $cached ??= as_has_scheduled_action( self::ASYNC_DELETION_ACTION );
}
Both existing callers are admin UI render paths, so is_admin() is a safe gate. Happy to send a PR.
On every request — including front-end pageviews where no Stream UI is rendered — Stream issues two duplicate queries against the Action Scheduler table to check whether the async deletion is running.
Sample (table prefix anonymised):
Backtrace (abridged):
Root cause
Settings::__constructeagerly populates$this->options = $this->get_options()on theinithook (priority 9).get_options()needs defaults,get_defaults()derives them by walkingget_fields(), andget_fields()happens to embed two pieces of dynamic UI state — thetypeand thedescof thedelete_all_recordsfield — both of which callAdmin::is_running_async_deletion()(classes/class-admin.php:716), which in turn callsas_has_scheduled_action()with no caching.The check is purely admin UI state: its only two callers decide whether to render a "deletion in progress" warning on the Stream settings screen. Computing it on front-end pageloads is unnecessary, and computing it twice in the same admin pageload is redundant.
Impact
Two DB hits on every pageload of every site that has Stream active, regardless of where the request is heading. Individually cheap (~0.5 ms each, indexed query), but the queries serve no purpose outside the Stream settings screen.
Suggested fix
Short-circuit
Admin::is_running_async_deletion()outside admin context, and memoise per-request:Both existing callers are admin UI render paths, so
is_admin()is a safe gate. Happy to send a PR.