What a freeze blocks
| Surface | During a freeze |
|---|---|
| wp-admin | Any POST, any link carrying a nonce, post-new.php and upgrade.php get 423. Screens still open |
| admin-ajax | Blocked unless the action only reads (heartbeat, media queries, link search, screen preferences and similar) |
| REST API | POST, PUT, PATCH, DELETE get 423. GET works. Saving editor preferences to /wp/v2/users/me is allowed, other profile changes are not |
| Front end | Form posts (comments, checkouts, contact forms) get 503 with Retry-After. wp-login.php is never blocked |
| XML-RPC | Off |
| WP-Cron | Held back (optional) |
| Automatic updates | Off |
| WP-CLI | Never blocked |
Users marked exempt when the freeze starts, users on the allow-list, and anyone a filter lets through, are not blocked at all. Requests the allow-list lets through are logged as allowed.
The table is for the default scope, Everything. A scope can freeze only part of it.
Scopes
A freeze covers one scope. Every request, ability and scheduled task belongs to one of two areas, and a freeze stops it only when its scope covers that area.
| Scope | --scope= | Refused | Keeps working |
|---|---|---|---|
| Everything (default) | all | Content, settings, plugins, themes and updates | Reading, logging in |
| Content only | content | Posts, pages, media, comments, terms, menus, orders, form entries and anything not recognised as settings or code | Options, plugins, themes, widgets, templates, user accounts, updates |
| Settings and code only | code | Options, plugins, themes, widgets, templates, fonts, updates, user accounts and roles | Editing content, comments, forms, checkouts |
A freeze stored before scopes existed is Everything. Scopes are one of the three above, or one added with the axle_read_only_mode_scopes filter.
How a request is sorted. Settings and code is what the built-in rules recognise: the admin screens for options, plugins, themes, updates, widgets, users and the customizer, anything posted from a Settings API form, a plugin settings page whose name says settings, options, config or license, REST routes such as /wp/v2/settings, /wp/v2/plugins, /wp/v2/themes, /wp/v2/templates, /wp/v2/global-styles and /wp/v2/users, the ajax actions that install or update code, and abilities whose name says option, setting, plugin, theme, update, user, role, widget or template. Everything else is content, so a content freeze errs on the side of stopping. If a plugin screen is sorted wrongly, fix it with axle_read_only_mode_request_area or one of the lists below.
What each part of a freeze does per scope
| Part | Everything | Content only | Settings and code only |
|---|---|---|---|
| wp-admin, REST, admin-ajax, abilities | As in the table above | Content writes refused, code writes allowed | Code writes refused, content writes allowed |
| Block editor | Locked | Locked | Not locked |
| Visitor submissions (comments, forms, checkouts, sign-ups) | Paused, if "visitor submissions" is on | Paused, if it is on | Open: they are content |
| XML-RPC | Off | Off | On, without wp.setOptions, wp.newUser, wp.editProfile and wp.deleteUser |
| Scheduled tasks (if "scheduled tasks" is on) | All held | All held, except update checks and site checks | Only update checks and site checks held |
| Automatic updates | Off | On | Off |
| The allow-list | Lets through what would be refused | Same, and only for content | Same, and only for settings and code |
A request the scope does not cover is left alone: it is not blocked, not logged and not counted as allowed. The scope shows in the status badge ("Content frozen", "Settings and code frozen"), the notice on every screen, the admin bar, the blocked list (an "Affects" column, and area in wp axle-read-only-mode log and the get-blocked ability), the history, the start and end emails, wp axle-read-only-mode status, and the abilities.
Before you freeze
The freeze screen has a Before you freeze card. It shows what a freeze with the chosen length, start and scope would interrupt right now, and updates when you change them. It informs and never blocks. Each item has a tone and a word.
| Item | Looks at | Words |
|---|---|---|
| Editing | Posts opened in the editor in the last 150 seconds (post locks): who, which post, how long ago. Not you | Editing now (warning), Clear (good), Not affected (neutral, when the scope leaves content alone) |
| Scheduled posts | Posts due inside the freeze window. With scheduled tasks held they publish when the freeze ends | Will be late (warning), Will publish (info), Clear (good) |
| Orders | WooCommerce orders waiting for payment, created in the last two hours. Only when WooCommerce is active | Mid-checkout (warning), Not affected (neutral), Clear (good) |
| Scheduled tasks | Tasks due inside the window that the scope would hold, grouped by name | Delayed (info), Clear (good) |
A freeze with no end time looks one day ahead. If anyone else is editing, the freeze button asks first and says how many people. The admin bar "Freeze site for 1 hour" stays one click, with no question: afterwards the notice on the freeze screen says how many people were editing, because it can be undone with one click and an extra step on every quick freeze would be the more annoying choice.
wp axle-read-only-mode check prints the same items, and the axle-read-only-mode/check-freeze ability returns them. Other plugins add items with the axle_read_only_mode_checks filter:
add_filter( 'axle_read_only_mode_checks', function ( $checks ) {
$checks['my_backup'] = function ( array $context ) {
if ( ! my_backup_is_running() ) {
return null;
}
return array(
'tone' => 'warning',
'word' => 'Backup running',
'title' => 'A backup is running',
'detail' => 'Freezing now stops it from writing its log.',
);
};
return $checks;
} );
Allow-list
Some things need to keep working during a freeze: a payment webhook, one form, a REST route, an AJAX action, a person. Tools > Read-Only Mode > Settings > Keep these working while frozen lists them, so nobody needs a code filter.
| Entry | Example | What it lets through |
|---|---|---|
| REST routes | /wc/v3/webhooks | POST, PUT, PATCH and DELETE to routes that start with it. A leading /wp-json and a query string are dropped. /wp and /wp/v2 are refused: they would let every core write through |
| Front-end paths or query arguments | /checkout/order-pay, ?wc-api=, ?callback=gravityformsstripe, /pay?token=abc | Form posts to addresses that start with the path, or that carry the query argument (any value, or the one given). / alone and full addresses are refused |
| Ajax actions | my_plugin_save_cart | admin-ajax.php requests with that action, lower case |
| Users | editor-in-chief, [email protected], 12 | Everything, like the "Let me keep editing" tick. One login, email address or ID per line |
Each entry is checked when saved. Lines that fail are left out and listed with the reason, and the rest are saved. Under each field, every saved entry says what it matches: how many registered REST routes start with it, whether a plugin handles the AJAX action, who the user is.
The settings run first and the existing filters run after them, so code can still say yes or no: axle_read_only_mode_allow_request, axle_read_only_mode_allow_rest_request, axle_read_only_mode_allowed_ajax_actions and axle_read_only_mode_user_can_bypass receive the answer of the settings and can change it.
Suggestions. When WooCommerce, Stripe for WooCommerce or Gravity Forms is active, the Settings tab shows common entries with an Add button. Nothing is ever added without a click. Payment providers retry webhooks that get a "back soon" answer, so add a webhook only if its provider does not retry. The axle_read_only_mode_allow_suggestions filter changes the list.
The log. A request the allow-list or a filter lets through is logged with the result allowed (and via: settings or code), next to the blocked ones. Only the first 25 per freeze are written, so a busy webhook does not turn into a database write per request. The axle_read_only_mode_allowed action still fires for every one. The count of blocked attempts never includes them.
On a network. A network freeze uses the network’s allow-list (Network Admin > Settings > Read-Only Mode > Settings) and ignores the site lists, so a site admin cannot open a hole in it. A site freeze uses the site’s list.
Tools > Read-Only Mode > Settings > Email when a freeze starts or ends. Off by default. Choose the recipients (the admin email when empty) and the events:
| Event | Sent when |
|---|---|
started | A freeze starts right away |
ended | Someone ends a freeze, from the screen, the admin bar, WP-CLI or an ability |
expired | A freeze reaches its end time and ends by itself |
extended | A freeze is made longer |
scheduled_start | A freeze that was set for later starts |
The email is plain text, sent with wp_mail(). It says who, why and until when. For ended and expired it says how many attempts were blocked and links to the list. The same event sends at most one email per minute, so a script that freezes and unfreezes in a loop cannot flood an inbox. A freeze ends and a scheduled one starts on the first request after the time, so a quiet site can be a little late.
A network freeze emails the network admin (or the recipients on the network’s Settings tab) and says which sites it covers.
Site Health
Tools > Site Health has a Read-only mode test:
| Status | When |
|---|---|
| Good | The site is not frozen. A scheduled freeze is mentioned |
| Recommended | A freeze with an end time is running, or a freeze with no end time is less than 6 hours old |
| Critical | A freeze ran more than 30 minutes past its end time, a freeze with no end time has lasted more than 6 hours, or AXLE_READ_ONLY_MODE in wp-config.php forces the freeze |
A freeze ends on the first request after its end time, so one that runs well past it means something is holding it on, such as a filter. On multisite the network freeze counts the same way for each site it covers. Both limits have filters, and wp axle-read-only-mode health runs the check from the command line. The Info tab has an Axle Read-Only Mode section with the state, end time, allow-list counts and email settings.
Privacy
The log and history hold user IDs, and the log holds the addresses people tried to post to or that the allow-list let through. If you turn on emails, the recipients are kept in the settings. Query strings are never stored, because they can hold email addresses or tokens. Both take part in Tools > Export Personal Data and Tools > Erase Personal Data: erasing keeps the entry and removes the person from it. Settings > Privacy gets suggested text for your privacy policy.
Multisite
A network admin can freeze every site, or chosen sites, from Network Admin > Settings > Read-Only Mode. Each site admin can still freeze their own site from Tools.
- A network freeze applies on every site it covers and overrides the site. A site that unfreezes itself is still frozen by the network. It shows "frozen by the network" in its notice, admin bar and messages, and offers no way to lift it.
- A network freeze and a site freeze are separate. Each has its own start, end, reason and history. The site is frozen while either one is running.
- The network keeps its own allow-list, email settings and history as network options. A network freeze uses the network’s allow-list and ignores the site lists.
- Logs stay per site. Each site logs what it blocked, and the network screen reads them from the start of the network freeze: a table per site and one list across the network. It reads the 100 most recently active sites (
axle_read_only_mode_network_log_sites). - Activation. Network-activate the plugin so a network freeze reaches every site, and so the Network Admin screen exists. If it is only active on some sites, a network freeze applies on those and not on the others, and the network screen says so.
- WP-CLI. See above:
--network,--sitesand the global--url. - Constants.
AXLE_READ_ONLY_MODEin wp-config.php freezes the whole network, with no end time. - Capability.
manage_network_options, changed withaxle_read_only_mode_network_capability.