A WordPress revision stores three columns: post_title, post_content and post_excerpt. Everything else about the post, including custom fields, taxonomy terms, the featured image, the page template and the menu order, is not in the revision at all.
That single fact explains most of the surprises people hit with revisions, and none of the guides that rank for this phrase state it. Restoring a revision rewrites three fields on the live post and leaves the rest exactly as it is right now.
All core code below was read from WordPress/wordpress-develop trunk on 2026-09-28.
What a revision actually stores
_wp_post_revision_fields() in wp-includes/revision.php builds the list, and it is short:
$fields = array(
'post_title' => __( 'Title' ),
'post_content' => __( 'Content' ),
'post_excerpt' => __( 'Excerpt' ),
);
The function then runs the _wp_post_revision_fields filter, and immediately strips a fixed list of columns back out regardless of what the filter added:
foreach ( array( 'ID', 'post_name', 'post_parent', 'post_date', 'post_date_gmt',
'post_status', 'post_type', 'comment_count', 'post_author' ) as $protect ) {
unset( $fields[ $protect ] );
}
So a plugin cannot version the author, the slug or the publish date through that filter. Core removes them after the filter runs, not before.
The official documentation is wrong about this. The wordpress.org Revisions article (fetched 2026-09-28) says under Revision Storage Method that "By default, WP keeps track of the changes to title, author, content, excerpt". post_author is on the disallowed list above, so it is never versioned.
The row itself is a normal post. _wp_post_revision_data() sets post_type to revision, post_status to inherit, post_parent to the real post ID, and the slug to {ID}-revision-v1, or {ID}-autosave-v1 for an autosave. The official doc still describes the older {parent ID}-revision(-#) naming.
Why "Restore This Revision" does not bring your layout back
wp_restore_post_revision() collects the revisioned fields and hands them to wp_update_post():
$update['ID'] = $revision['post_parent'];
$post_id = wp_update_post( $update );
There is no taxonomy call in that function and no bulk meta copy. If you changed the categories, swapped the featured image, edited an ACF field or switched the page template between the two saves, the restore does not touch any of it.
This matters most on sites built with a page builder that stores layout in postmeta rather than in post_content. The revision list will show entries, the diff will look empty or near empty, and restoring will change nothing visible, because the content people actually edited was never in the revisioned columns.
Elementor and similar builders keep their document in _elementor_data. Beaver Builder keeps it in _fl_builder_data. Neither is post_content, so neither is versioned by default.
The one exception, added in 6.4
Since WordPress 6.4 a meta key can be revisioned, but only if it opts in. wp_post_revision_meta_keys() walks the registered meta for the post type and keeps the keys whose registration set revisions_enabled:
foreach ( $registered_meta as $name => $args ) {
if ( $args['revisions_enabled'] ) {
$wp_revisioned_meta_keys[ $name ] = true;
}
}
Meta added with plain add_post_meta() is not registered, so it is never in this list. Meta registered with register_post_meta() is only in the list if the developer passed 'revisions_enabled' => true, which is not the default.
There is a sharp edge in the restore path for the keys that do opt in. wp_restore_post_revision_meta() deletes the current value before copying the old one:
// Clear any existing meta.
delete_post_meta( $post_id, $meta_key );
_wp_copy_post_meta( $revision_id, $post_id, $meta_key );
If the revision has no value for that key, the delete still runs and the copy adds nothing. The current value is gone and there is no undo for it.
Custom post types have no revisions unless someone asked for them
WP_Post_Type::add_supports() sets the default when register_post_type() is called without a supports argument:
add_post_type_support( $this->name, array( 'title', 'editor', 'autosave' ) );
revisions is not in that list. Posts and pages get revisions because core registers them explicitly, and a custom post type gets them only if its author included 'revisions' in supports.
wp_save_post_revision() checks post_type_supports( $post->post_type, 'revisions' ) and returns null when it fails. That is silent: there is no notice, and the editor simply never shows a revision count.
WooCommerce products, most CPTs generated by a plugin's UI, and reusable template parts vary here. Check before you rely on it rather than assuming.
Restoring a revision creates another revision
wp_restore_post_revision() calls wp_update_post(), which is an ordinary post update, so the revision hook fires again on the way out. The version you restored away from is saved as a new revision, which is genuinely useful and also means the row count goes up every time you use the feature.
It also writes _edit_last to the user doing the restore. The revision list therefore shows the restorer as the most recent editor, not the person who wrote the content that is now live.
Why the table keeps growing when you set a limit
WP_POST_REVISIONS is read once in wp_revisions_to_keep():
$num = WP_POST_REVISIONS;
if ( true === $num ) {
$num = -1;
} else {
$num = (int) $num;
}
true and -1 mean keep everything, which is the default. false casts to 0 and turns revisions off, and any positive integer is the cap.
The pruning happens at the end of wp_save_post_revision(), and it counts every child row including autosaves:
$delete = count( $revisions ) - $revisions_to_keep;
It then walks the oldest rows and skips any autosave:
if ( str_contains( $revisions[ $i ]->post_name, 'autosave' ) ) {
continue;
}
The skip does not extend the window. An autosave that falls inside the delete slice consumes one of the deletions without anything being deleted, so the stored row count sits above the cap you set. On a multi-author site with one autosave per user, that gap is one row per autosave.
The cap also only applies on save. Lowering WP_POST_REVISIONS from true to 5 does not trim anything until each post is edited again.
Where to set the limit
In wp-config.php, above the line that requires wp-settings.php:
define( 'WP_POST_REVISIONS', 5 );
Or with WP-CLI:
wp config set WP_POST_REVISIONS 5 --raw
--raw matters. Without it WP-CLI writes a quoted string, so the file gets define( 'WP_POST_REVISIONS', '5' ), and the string 'false' is especially worth avoiding: (int) 'false' is 0, so it happens to disable revisions, but it reads as if it were the boolean and nothing tells you otherwise.
For a per-post-type limit, use the wp_{$post_type}_revisions_to_keep filter rather than the constant. It overrides both the constant and the generic wp_revisions_to_keep filter.
Deleting the revisions you already have
The method most guides teach is a raw DELETE FROM wp_posts WHERE post_type = 'revision' in phpMyAdmin. It works on the posts table and leaves the rest behind.
Revision rows can own postmeta rows, both from the 6.4 revisioned meta feature and from plugins that copy meta onto revisions themselves. A raw delete on wp_posts orphans those rows, and nothing in core ever cleans them up.
Core's own deletion path avoids that. wp_delete_post_revision() calls wp_delete_post(), and the comment in wp_delete_post() explains why it takes the long route for revisions:
// Use wp_delete_post (via wp_delete_post_revision) again. Ensures any meta/misplaced data gets cleaned up.
Note what that same function does for you already: permanently deleting a post runs a raw query for its revision children and deletes each one properly. Trashing a post does not, so revisions of trashed posts stay until the trash is emptied.
To count and clear them from WP-CLI:
wp post list --post_type=revision --format=count
wp post list --post_type=revision --format=ids | xargs wp post delete --force
That routes every delete through wp_delete_post(), so the meta goes with it.
Rehearse the purge on a copy first
A revision purge on a busy site is a large, irreversible write. It is also the kind of change whose damage is invisible for weeks, because nobody opens the revision list of an old page until they need it.
Take a copy of the live site, run the purge there, and check three things: that the page builder content still renders, that the posts you care about still have the revisions you expected to keep, and how long the query actually takes on your real row count. Our Cloner plugin makes the copy, and the CLI and SSH and SFTP access give you a real wp binary on the copy to run the commands above.
The timing number is the one worth having. A DELETE across a few hundred thousand rows on a shared database behaves very differently from the same query on a quiet sandbox, and measuring it on a copy tells you whether to schedule a maintenance window.
When revisions are the wrong tool
Revisions are not a backup. They cover three columns of one post, they are stored in the same database that would be lost with the site, and they are deleted along with the post.
They are also not an audit log. They record the editing user and timestamp for content changes only, so a settings change, a plugin update or a taxonomy edit leaves no revision behind.
If what you want is the ability to put a whole site back to how it was on Tuesday, that is a backup and a restore test, not a revision. We wrote about testing that a backup actually restores separately, and about rehearsing plugin updates on a copy for the same reason.
The short version
A revision is post_title, post_content and post_excerpt, plus any meta key that explicitly opted in since 6.4. Restoring one rewrites those fields on the live post and leaves the taxonomy, the featured image and the rest of your meta untouched.
The cap set by WP_POST_REVISIONS is applied on save, counts autosaves it then refuses to delete, and does nothing retroactively. If you are about to clear a large table of them, delete through wp_delete_post() rather than SQL, and try it on a copy before you try it on production.
