


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. Site 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.
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 site-read-only-mode on --minutes=30 --reason="Moving servers"
$ wp site-read-only-mode off
$ wp site-read-only-mode status
$ wp site-read-only-mode extend 30
$ wp site-read-only-mode logWP-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( 'SITE_READ_ONLY_MODE', true );
define( 'SITE_READ_ONLY_MODE_REASON', 'Moving servers' );Install
Upload
- Download site-read-only-mode.zip (v2.0.1).
- In wp-admin, go to Plugins > Add New Plugin > Upload Plugin and choose the zip.
- Click Install Now, then Activate.
WP-CLI
Install and activate the latest release in one command:
wp plugin install https://github.com/aryanjasala/site-read-only-mode/releases/download/v2.0.1/site-read-only-mode.zip --activateCompatibility
| WordPress | PHP | Status |
|---|---|---|
| WordPress 7.1 | PHP 8.3 | Tested |
| WordPress 6.9 | PHP 8.3 | Tested |
| WordPress 6.6 | PHP 7.4 | Tested |
Questions
How is this different from a maintenance mode plugin?
Maintenance mode shows visitors a placeholder page instead of the site. Site 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 site_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 site_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.
site_read_only_mode_default_durationFilter- Length when none is given: the admin bar quick freeze,
wp site-read-only-mode onand the PHP API. Also preselected on the screen. Default 60,0for no end( int $minutes ) site_read_only_mode_durationsFilter- Lengths offered on Tools > Site Read-Only Mode. Default 30, 60, 120, 240, 480 and 0( int[] $minutes )
site_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 )
site_read_only_mode_is_frozenFilter- Freeze or unfreeze from outside, such as one switch for every site. Add it from a must-use plugin.
SITE_READ_ONLY_MODEalways wins( bool $frozen, array $state ) site_read_only_mode_messageFilter- What logged-in users see( string $message, array $state )
site_read_only_mode_visitor_messageFilter- What visitors get when they send a form( string $message, array $state )
site_read_only_mode_user_can_bypassFilter- Let a user keep working during a freeze( bool $exempt, int $user_id )
site_read_only_mode_allowed_ajax_actionsFilter- Admin-ajax actions that only read and should keep working( string[] $actions )
site_read_only_mode_allow_requestFilter- Let one wp-admin or front-end request through, such as a single sign-on callback( bool $allow, string $where, string $source )
site_read_only_mode_allow_rest_requestFilter- Let one REST request through( bool $allow, WP_REST_Request $request )
site_read_only_mode_read_only_routesFilter- REST route prefixes that only read even when called with POST. Default
/wp/v2/block-renderer/( string[] $routes ) site_read_only_mode_writing_screensFilter- Admin screens blocked even when only opened. Default
post-new.phpandupgrade.php( string[] $screens ) site_read_only_mode_logged_visitorsFilter- Anonymous blocked attempts logged per freeze. Default 10,
0for none( int $limit ) site_read_only_mode_log_sizeFilter- Blocked attempts the log keeps. Default 100( int $size )
site_read_only_mode_history_sizeFilter- Past freezes the history keeps. Default 10( int $size )
site_read_only_mode_capabilityFilter- Who may freeze and unfreeze. Default
manage_options( string $capability ) site_read_only_mode_startedAction- A freeze started or was scheduled( array $state )
site_read_only_mode_endedAction- A freeze ended:
user,wp-cliorexpired( array $state, string $ended_by ) site_read_only_mode_extendedAction- A freeze got longer( array $state, int $minutes )
site_read_only_mode_blockedAction- Every blocked request, including the ones the log leaves out. For an outside log or an alert channel( string $where, string $source, int $user_id )
Changelog
- v2.0.1
- FixRefuse abilities that write during a freeze on WordPress 6.9 and 7.0
- v2.0.0
- ChangedRename to Site Read-Only Mode for WordPress.org (breaking change)
- v1.3.0
- FixMake the abilities visible to MCP agents, and let the MCP adapter run them during a freeze
- NewAdd abilities so AI agents can check and control a freeze, and refuse other abilities that write during one
Every release, with notes: GitHub releases.