[wp-trac] [WordPress Trac] #24958: Large number of revisions cause memory exhaustion
WordPress Trac
noreply at wordpress.org
Fri Sep 26 10:59:51 UTC 2025
#24958: Large number of revisions cause memory exhaustion
------------------------------------+------------------------------
Reporter: jshreve | Owner: adamsilverstein
Type: defect (bug) | Status: assigned
Priority: low | Milestone: Future Release
Component: Revisions | Version: 3.6
Severity: normal | Resolution:
Keywords: has-patch dev-feedback | Focuses:
------------------------------------+------------------------------
Comment (by frkly):
I came across this ticket while investigating an issue where updating a
post with many revisions triggered the following PHP out-of-memory error:
{{{
PHP Fatal error: Allowed memory size of *** bytes exhausted (tried to
allocate *** bytes) in /var/www/html/wp-includes/class-wpdb.php on line
2322
}}}
The root cause seems to be that, solely for the purpose of checking
whether the current post has changed compared to the latest revision and
therefore needs to be saved as a new revision, WordPress core retrieves
all revisions, including those that are not the latest.
In @oglekler’s example, in order to identify “which revision is the
latest,” WordPress uses get_posts to load around 900 revisions into
memory, and that is what leads to the memory exhaustion.
The call sequence is:
{{{
wp_update_post
→ wp_insert_post
→ wp_after_insert_post
→ wp_save_post_revision_on_insert
→ wp_save_post_revision
→ wp_get_post_revisions
→ get_children
→ get_posts
}}}
`wp_get_post_revisions` literally fetches all post data of all revisions,
but in practice only the `post_name` is used except for the latest
revision.
Notably, because this path ultimately goes through `get_posts()`, it
primes the object cache for every retrieved revision (and often post
meta/terms, depending on query flags). On sites with a persistent object
cache (e.g. Redis/Memcached), those hundreds of revision entries can also
be stored in the external cache, increasing memory/IO footprint. In other
words, the current implementation can end up caching data for revisions
that are never needed (e.g. “from 900 versions ago”), which unnecessarily
consumes resources.
In the core code `wp-includes/revision.php`, it is currently implemented
like this:
https://core.trac.wordpress.org/browser/trunk/src/wp-
includes/revision.php?rev=59715#L163
{{{#!php
$revisions = wp_get_post_revisions( $post_id );
if ( $revisions ) {
// Grab the latest revision, but not an autosave.
foreach ( $revisions as $revision ) {
if ( str_contains( $revision->post_name,
"{$revision->post_parent}-revision" ) ) {
$latest_revision = $revision;
break;
}
}
if ( isset( $latest_revision ) && ....
}}}
What we actually need here is the ID of the single latest revision among
those whose `post_name` matches a specific pattern (i.e.,
`{$post_id}-revision%`, excluding autosaves). Loading the entire set of
revisions is unnecessary for that.
A more efficient approach would be:
{{{#!php
// Fetch only the single latest non-autosave revision ID within the
matching post_name pattern.
$latest_id = $wpdb->get_var( $wpdb->prepare(
"
SELECT ID
FROM {$wpdb->posts}
WHERE post_parent = %d
AND post_type = 'revision'
AND post_status='inherit'
AND post_name LIKE %s
ORDER BY post_date DESC, ID DESC
LIMIT 1
",
$post_id,
$wpdb->esc_like( "{$post_id}-revision" ) . '%'
) );
$latest_revision = $latest_id ? get_post( $latest_id ) : null;
if ($latest_revision && ....
}}}
I only discovered this issue today and have not done a deep analysis, and
I’m not an expert in this area.
I hope others can review this approach and consider whether it’s a
practical improvement for core.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/24958#comment:25>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list