My working notes on WP Rocket for one site include a table of cache stores. The used CSS row, which belongs to the Remove Unused CSS (RUCSS) feature, says the data is stored in the database table {prefix}_wpr_rucss_used_css, with the remark "no function exists in 3.21". That remark had been read as "used CSS can only be cleared by hand". The correction is dated 29 August 2026, and it is blunt: that reading was wrong.
The table itself does not lie. rocket_clean_used_css() genuinely does not exist. The mistake was the jump from "no function" to "by hand", because the Clear Used CSS button in the panel does not call a function either. It fires a filter, and a filter can be applied from WP-CLI. The trap is in how this route fails: without --user, the command answers {"status":"die"}, deletes nothing, and gives no sign that anything went wrong.
My older post about RUCSS breaking mobile menus suggests pressing Clear Used CSS in the panel whenever a RUCSS setting changes. This one is about running the same clear without the panel, and about the empty results around it that are easy to mistake for success.
The button ends in a filter
I traced the chain from the button to the deletion on WP Rocket 3.21.1, and it looks like this:
admin_post_rocket_clean_saas
-> WP_Rocket\Engine\Saas\Admin\Subscriber::clean_saas
-> WP_Rocket\Engine\Saas\Admin\Clean::clean_saas
-> apply_filters( 'rocket_saas_clean_all', [] )
-> WP_Rocket\Engine\Optimization\RUCSS\Admin\Subscriber::truncateThe link that matters is the fourth line. The clearing is driven by the rocket_saas_clean_all filter, and a filter can be run from any code, WP-CLI included, without going through the button. The nonce checked in clean_saas() does not need faking either, because it is CSRF protection for the browser request, not part of the clearing logic. If your WP Rocket version is different, trace the chain again before trusting the names above.
A die that looks like a finished run
WP-CLI runs WordPress with no logged-in user unless you give it the global --user flag. For this filter the flag is required, and leaving it out gives no signal at all. Without --user, the same call answers like this:
wp eval 'echo wp_json_encode( apply_filters("rocket_saas_clean_all", []) );'
# {"status":"die"}That answer changes nothing. The table rows and the files stay exactly where they were. die here is the capability check failing, not an error, so a run that did nothing at all looks like a run that finished.
With --user set to my own admin user ID on that site, the same command answers differently:
wp --user=<ADMIN_ID> eval 'echo wp_json_encode( apply_filters("rocket_saas_clean_all", []) );'
# {"status":"success","message":"<strong>WP Rocket</strong>: Used CSS cache cleared!"}The wp_json_encode is only there so the array the filter returns gets printed in readable form. What you check is the content: success with a message, not die with none.
Count both stores, before and after
Because die is so easy to miss, the rule I set for this command is simple: always count rows and files before and after running it. The filter empties two stores at once. On that site the used CSS table went from 4,049 rows to 0, and the files in wp-content/cache/used-css/ went from 711 to 0.
Both need counting, because measuring the column alone misleads. SUM(LENGTH(css)) reads 0 because the CSS lives in the files, not in the table column. A simplified version, with a table prefix you will need to adjust:
wp db query "SELECT COUNT(*) FROM wp_wpr_rucss_used_css"
find wp-content/cache/used-css -type f | wc -lI back up the table before running it; the dump is about 1.2 MB. Once used CSS is empty, the page cache gets purged, in that order. The reason is in the next section.
The correct result can look like breakage. Every page drops its inline <style id="wpr-usedcss"> block and links the theme's compiled stylesheet instead. That is correct, not broken. A page with no wpr-usedcss and a real stylesheet link is the healthy intermediate state. Measured across eight URLs, the theme stylesheet was present on all eight, and the gzipped homepage went from 54,859 to 33,492 bytes.
Used CSS rows first, page cache after
rocket_clean_domain() does not just empty a directory. It kicks off Rocket's preload, which starts re-crawling and re-caching pages immediately. Purge the page cache first and the preload bakes the old used CSS into the freshly cached HTML, and the row you then delete was the only record that would have told Rocket to regenerate it. The result is pages cached with a stale <style id="wpr-usedcss"> and not a single row left in the table to explain why.
This was measured on a deploy that touched four articles. One theme class and the .border-y utility were missing from the inlined used CSS on two of the four articles, while a per-URL SELECT against the table returned nothing for any of them. Reversing the order and warming each page twice fixed all four. The pages came back with a real <link> to the theme stylesheet, and Rocket then regenerated correct used CSS within about a minute.
That order is what the fix code in the same notes follows. A simplified version:
// used CSS rows FIRST, this order is not cosmetic
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->prefix}wpr_rucss_used_css WHERE url = %s",
$url
) );
rocket_clean_domain(); // page cache, kicks off preload
rocket_clean_minify();
rocket_clean_cache_busting();The rocket_clean_minify() call underneath is in line with the rule in the same notes: after any CSS or JS change, the page cache, the minified assets, and used CSS all have to be cleared.
The check that proves it is not "is the class in the HTML". A class present in the markup says nothing about whether its rule was kept. What you count is the rule inside <style id="wpr-usedcss">, in HTML fetched straight from the origin. The class and stylesheet names below have been swapped for neutral ones:
curl -s --resolve host:443:<origin> https://host/url/ -o p.html
grep -o '\.article-body' p.html | wc -l # the rule, inside <style id="wpr-usedcss">
grep -c 'theme.css' p.html # 1 = RUCSS off for this page (fine, full CSS)Watch for the legitimate variant too. An article with no pull quotes should be missing .border-y, so check a selector the page actually uses before calling its absence a bug.
Per-URL deletes: the url column has no trailing slash
Clearing the whole table has a price. On a plugin hardening pass the same day as the correction, used CSS was deliberately left alone: 4,046 rows, no CSS had changed, and wiping them regenerates every page on the site at once.
The same notes set the rule for deploys: delete used CSS only for the changed URLs, not the whole table. There were 3,863 rows at the time, and wiping all of them makes every page on the site regenerate at once. In that case 47 rows covered 40 URLs. Two rows for one URL is normal: one for desktop and one for is_mobile.
The per-URL route has its own misleading zero. Measured on 18 August 2026, the url column in the used CSS table stores https://example.com/hub, never https://example.com/hub/, even though every live URL on that site has a trailing slash and home_url("/hub/") returns one. A LIKE '%/hub/' query matched 0 of 4,326 rows, and that zero read exactly like "these pages have no used CSS row". The conclusion is wrong and so is its consequence: the purge gets skipped.
Do not fall back to LIKE '%hub%' either. That matched 89 rows, because 34 child articles live under the same prefix and their rows are fine. Exact match only, with the trailing slash stripped first. I added the $t line so the snippet is complete:
$t = $wpdb->prefix . 'wpr_rucss_used_css';
$bare = untrailingslashit( home_url( "/$slug/" ) ); // required
$wpdb->query( $wpdb->prepare( "DELETE FROM `$t` WHERE url = %s", $bare ) );Before believing a zero, look at the column. SELECT url FROM $t LIMIT 6 settles it in one query. Once the rows are gone and the page cache is purged, warm each page twice, since the first hit regenerates and the second serves it, then measure again.
An empty result is not proof
Almost every trap here takes the form of an empty result that reads like "done" or "nothing to do". {"status":"die"} looks like a finished run while deleting nothing. SUM(LENGTH(css)) reads 0 while the CSS still sits in the files. An empty per-URL SELECT after a reversed purge order does not mean the page is clean, and a LIKE '%/hub/' that matches 0 of 4,326 rows does not mean the page has no used CSS.
The remedy is the same for all of them: run the filter with --user, count rows and files on both sides, delete the used CSS rows before rocket_clean_domain(), and look at the column before trusting a zero.