Free on WordPress.org

Read-Only Mode

Freeze the site during a migration or deploy. Visitors keep reading; nothing written gets lost.

Use it when

  • you move the site to a new host.
  • you copy a database from staging over the live site.
  • you run a deploy that changes content or settings.
The block editor during a freeze
The block editor during a freeze
Tools > Read-Only Mode during a freeze, with countdown and blocked attempts
Tools > Read-Only Mode during a freeze, with countdown and blocked attempts
What visitors see when they send a form during a freeze
What visitors see when they send a form during a freeze
Tools > Read-Only Mode > Settings, with the allow-list and the email choices
Tools > Read-Only Mode > Settings, with the allow-list and the email choices

What it does

When you move a site to a new server, or replace the live database with one from staging, anything saved on the old copy during the move is lost: a post an editor was finishing, a comment, a form entry, a WooCommerce order.

Maintenance mode hides the whole site from visitors. Axle Read-Only Mode keeps the site online but makes it read-only:

  • Editors can open and read everything in wp-admin, but cannot save. The block editor disables Save and Publish and shows why. People who already have the editor open are locked within a minute, through the WordPress heartbeat, and unlocked again when the freeze ends.
  • Admin actions that change data are refused: saving settings, uploading media, installing or updating plugins, trashing posts. Automatic updates are paused.
  • The REST API and XML-RPC refuse anything that writes, with HTTP status 423. Reading still works.
  • Visitor submissions (optional, on by default): comments, sign-ups, contact forms, checkouts and any other form post get a "back soon" page with HTTP 503 and a Retry-After header, which search engines understand.
  • Scheduled tasks (optional, on by default) are held back and run when the freeze ends.

Logging in always works, so an admin can always unfreeze.

Free and open source under the GPL. No pro version, no upsells, no ads in wp-admin, no tracking. And it stays that way.

Part of Axle by AryanX, a family of plugins for running WordPress sites safely.

See what was blocked

Every blocked attempt is listed: who tried, when, and what (a post save, an upload, a REST request, a contact form). After the move you know exactly which edits to redo on the new server. The last ten freezes are kept too, with how many attempts each one blocked.

Freeze safely

  • Pick an end time and the freeze ends by itself. A deploy script that fails halfway cannot leave the site frozen for days.
  • Running late? Add 30 minutes or an hour from the freeze screen or the admin bar. A live countdown shows the time left.
  • Schedule a freeze for later, for example the start of a maintenance window.
  • Tick "Let me keep editing" to exempt yourself.
  • Write your own message for visitors, for example "We are moving servers. Back by 3 pm."
  • Freeze for an hour straight from the admin bar (under the site name), and unfreeze from the admin bar or the notice on every screen.

From a deploy script

$ wp axle-read-only-mode on --minutes=30 --reason="Moving servers"
$ wp axle-read-only-mode off
$ wp axle-read-only-mode status
$ wp axle-read-only-mode extend 30
$ wp axle-read-only-mode log

WP-CLI itself is never blocked, so your scripts can keep importing and replacing data. Use --no-visitors or --no-cron to leave those running.

Or freeze from wp-config.php while files are being swapped:

define( 'AXLE_READ_ONLY_MODE', true );
define( 'AXLE_READ_ONLY_MODE_REASON', 'Moving servers' );

Install

  1. Download axle-read-only-mode.zip (v3.1.0).
  2. In wp-admin, go to Plugins > Add New Plugin > Upload Plugin and choose the zip.
  3. Click Install Now, then Activate.

Or with WP-CLI:

$ wp plugin install https://github.com/aryanjasala/axle-read-only-mode/releases/download/v3.1.0/axle-read-only-mode.zip --activate

To try it first, try.axlewp.com/axle-read-only-mode opens a private WordPress in your browser with this release and some demo content.

Compatibility

Checked by the end-to-end tests on every change. Unit tests run on PHP 7.4 to 8.4.
WordPressPHPStatus
WordPress 7.1PHP 8.3Tested
WordPress 6.9PHP 8.3Tested
WordPress 6.6PHP 7.4Tested

Questions

How is this different from a maintenance mode plugin?

Maintenance mode shows visitors a placeholder page instead of the site. Axle Read-Only Mode keeps the site fully visible and only stops changes. Use it when the site has to stay online during the move.

Does it stop every plugin’s forms?

It stops every form that posts to the site from the front end, every REST API write and every admin-ajax request that is not on the read-only list. A form that sends data straight to an outside service is not affected.

A read-only screen of my plugin stopped working during the freeze.

Admin-ajax requests are blocked unless they are known to be read-only. Add the action name with the axle_read_only_mode_allowed_ajax_actions filter.

Can someone keep working while the site is frozen?

Yes. Tick "Let me keep editing" when you freeze, or use the axle_read_only_mode_user_can_bypass filter.

For developers

Every default, limit and message has a filter, and every change fires an action. The full reference, with constants, abilities and WP-CLI, is in REFERENCE.md.

axle_read_only_mode_default_durationFilter
Length when none is given: the admin bar quick freeze, wp axle-read-only-mode on and the PHP API. Also preselected on the screen. Default 60, 0 for no end ( int $minutes )
axle_read_only_mode_durationsFilter
Lengths offered on Tools > Read-Only Mode. Default 30, 60, 120, 240, 480 and 0 ( int[] $minutes )
axle_read_only_mode_scopesFilter
The scopes a freeze can have. Each: label, status, description, covers (the areas it freezes, content and code). Keep all first ( array $scopes )
axle_read_only_mode_default_scopeFilter
Scope when none is chosen: the admin bar quick freeze, WP-CLI, abilities, the PHP API. Also preselected on the screen. Default all ( string $scope )
axle_read_only_mode_scopeFilter
The scope of the freeze in force ( string $scope, array $state )
axle_read_only_mode_request_areaFilter
Sort one request into content or code. $kind is admin, rest, ajax, ability, cron or front-end ( string $area, string $kind, string $subject, array $context )
axle_read_only_mode_code_screensFilter
Admin files that change settings, plugins, themes, updates or accounts ( string[] $screens )
axle_read_only_mode_code_routesFilter
REST route prefixes that change settings, plugins, themes, templates or accounts ( string[] $routes )
axle_read_only_mode_code_ajax_actionsFilter
Ajax actions that install, change or update code and settings ( string[] $actions )
axle_read_only_mode_code_ability_wordsFilter
Words in an ability name that mark it as settings or code ( string[] $words )
axle_read_only_mode_code_cron_hooksFilter
Scheduled events that check for updates or run site checks. Every other event counts as content ( string[] $hooks )
axle_read_only_mode_code_xmlrpc_methodsFilter
XML-RPC methods a settings and code freeze removes ( string[] $methods )
axle_read_only_mode_checksFilter
The checks on the Before you freeze card: ID to callable. Add yours, or unset a built-in one: editing, scheduled_posts, orders, cron. A callable gets the context (start, end, until, minutes, scope, cron, visitors, now, user_id) and returns an item (tone, word, title, detail, rows, count) or null ( array $checks, array $context )
axle_read_only_mode_check_itemsFilter
The items after every check ran ( array $items, array $context )
axle_read_only_mode_check_editorsFilter
The people the card says are editing. Each: post_id, title, user_id, name, time ( array $editors )
axle_read_only_mode_check_open_windowFilter
How far ahead the checks look when a freeze has no end time. Default one day ( int $seconds )
axle_read_only_mode_check_order_ageFilter
How recent an unpaid WooCommerce order must be to count as waiting for payment. Default two hours ( int $seconds )
axle_read_only_mode_extend_minutesFilter
The extend buttons. The first one is also the admin bar’s "Add 30 minutes". Default 30 and 60 ( int[] $minutes )
axle_read_only_mode_is_frozenFilter
Freeze or unfreeze from outside, such as one switch for every site. $frozen includes a network freeze. Add it from a must-use plugin. AXLE_READ_ONLY_MODE always wins ( bool $frozen, array $state )
axle_read_only_mode_messageFilter
What logged-in users see ( string $message, array $state )
axle_read_only_mode_visitor_messageFilter
What visitors get when they send a form ( string $message, array $state )
axle_read_only_mode_user_can_bypassFilter
Let a user keep working during a freeze ( bool $exempt, int $user_id )
axle_read_only_mode_allowed_ajax_actionsFilter
Admin-ajax actions that keep working: the read-only ones plus the ones on the allow-list. Add one, or take one off ( string[] $actions )
axle_read_only_mode_allow_requestFilter
Let one wp-admin or front-end request through, such as a single sign-on callback. $allow is already true when the allow-list lets the request through, so code can return false to stop it ( bool $allow, string $where, string $source )
axle_read_only_mode_allow_rest_requestFilter
Let one REST request through. Like the one above, it runs after the allow-list ( bool $allow, WP_REST_Request $request )
axle_read_only_mode_read_only_routesFilter
REST route prefixes that only read even when called with POST. Default /wp/v2/block-renderer/ ( string[] $routes )
axle_read_only_mode_writing_screensFilter
Admin screens blocked even when only opened. Default post-new.php and upgrade.php ( string[] $screens )
axle_read_only_mode_logged_visitorsFilter
Anonymous blocked attempts logged per freeze. Default 10, 0 for none. Requests let through have their own limit ( int $limit )
axle_read_only_mode_log_sizeFilter
Entries the log keeps, blocked and allowed. Default 100 ( int $size )
axle_read_only_mode_history_sizeFilter
Past freezes the history keeps. Default 10 ( int $size )
axle_read_only_mode_capabilityFilter
Who may freeze and unfreeze. Default manage_options ( string $capability )
axle_read_only_mode_logged_allowedFilter
Requests let through that the log keeps per freeze. Default 25, 0 for none ( int $limit )
axle_read_only_mode_allow_limitFilter
Entries each part of the allow-list holds. Default 50 ( int $limit )
axle_read_only_mode_validate_allow_entryFilter
Runs after the built-in checks of an allow-list entry. Return a message to refuse it. $type is rest, paths, ajax or users ( string $error, string $type, string|int $value )
axle_read_only_mode_allow_suggestionsFilter
The "Add" buttons on the Settings tab. Each: plugin, type, value, why. They are only suggestions: nothing is added without a click ( array $suggestions )
axle_read_only_mode_notify_eventsFilter
The events that send an email: started, ended, expired, extended, scheduled_start ( string[] $events, bool $network )
axle_read_only_mode_notify_recipientsFilter
Who gets the email. Default the addresses in the settings, or the admin email ( string[] $recipients, string $event, array $freeze, bool $network )
axle_read_only_mode_notify_subjectFilter
The email subject ( string $subject, string $event, array $freeze, bool $network )
axle_read_only_mode_notify_bodyFilter
The plain-text body. $context has minutes (extended), ended_by and blocked (ended) ( string $body, string $event, array $freeze, bool $network, array $context )
axle_read_only_mode_notify_intervalFilter
Seconds before the same event can send another email. Default 60, never less ( int $seconds, string $event )
axle_read_only_mode_health_overdue_minutesFilter
Minutes a freeze can run past its end time before Site Health calls it critical. Default 30 ( int $minutes )
axle_read_only_mode_health_open_ended_hoursFilter
Hours a freeze with no end time can last before Site Health calls it critical. Default 6 ( int $hours )
axle_read_only_mode_health_resultFilter
The Site Health result. $code is off, frozen, overdue, open_ended or constant ( array $result, string $code )
axle_read_only_mode_health_infoFilter
The fields of the Site Health > Info section ( array $fields )
axle_read_only_mode_network_capabilityFilter
Who may freeze and unfreeze the network. Default manage_network_options ( string $capability )
axle_read_only_mode_network_log_sitesFilter
Sites the network log reads, most recently active first. Default 100 ( int $limit )
axle_read_only_mode_network_site_picker_limitFilter
Sites the "chosen sites" list shows. Default 200 ( int $limit )
axle_read_only_mode_startedAction
A freeze started or was scheduled. $state['area'] is its scope: all, content or code ( array $state )
axle_read_only_mode_endedAction
A freeze ended: user, cli, ability or expired ( array $state, string $ended_by )
axle_read_only_mode_extendedAction
A freeze got longer ( array $state, int $minutes )
axle_read_only_mode_blockedAction
Every blocked request, including the ones the log leaves out. $area is content or code. For an outside log or an alert channel ( string $where, string $source, int $user_id, string $area )
axle_read_only_mode_checkedAction
The Before you freeze checks ran, from the screen, WP-CLI or an ability. $result has items, editing, attention and context ( array $result )
axle_read_only_mode_scheduled_startedAction
A freeze that was set for later has started. Fires on the first request after its start time ( array $state )
axle_read_only_mode_allowedAction
Every request the allow-list (settings) or a filter (code) let through a freeze, including the ones the log leaves out ( string $where, string $source, int $user_id, string $via, string $area )
axle_read_only_mode_allow_list_savedAction
The allow-list was saved, from the screen, WP-CLI or a suggestion button ( array $allow, bool $network )
axle_read_only_mode_notify_settings_savedAction
The email settings were saved ( array $notify, bool $network )
axle_read_only_mode_notifiedAction
An email was handed to wp_mail() ( string $event, string[] $recipients, string $subject, bool $sent, array $freeze, bool $network )
axle_read_only_mode_network_startedAction
A network freeze started or was scheduled ( array $state )
axle_read_only_mode_network_scheduled_startedAction
A network freeze that was set for later has started ( array $state )
axle_read_only_mode_network_extendedAction
A network freeze got longer ( array $state, int $minutes )
axle_read_only_mode_network_endedAction
A network freeze ended ( array $state, string $ended_by )

Changelog

3.1.0

  • New: check what a freeze would interrupt, and freeze part of the site
  • New: freeze a multisite network, allow-list, email and Site Health

3.0.0

  • Changed (breaking change): rename to Axle Read-Only Mode for WordPress.org

2.0.1

  • Fix: refuse abilities that write during a freeze on WordPress 6.9 and 7.0

Every release, with notes: GitHub releases.