If your WordPress save_post callback fires twice, first check whether the callback—or something it calls—runs wp_update_post(). That function saves the post and triggers the save hooks again, re-entering the callback. The standard fix is to remove that exact callback before the nested update and restore it afterward. Revisions can also account for a second invocation, so inspect the post ID and revision status rather than assuming every repeat has the same cause.
Why does the WordPress save_post hook fire twice?
The save_post action runs after a post or page is created or updated. Its $update argument indicates whether the post already existed. A callback attached to this action that calls wp_update_post() causes another save, which fires the hook again. If the callback makes the same update on every invocation, it can keep re-entering itself indefinitely. WordPress describes this behavior in its save_post hook reference and wp_update_post() reference.
As an Amazon Associate I earn from qualifying purchases.
A second invocation does not always mean recursion. When revisions are enabled, WordPress warns that save_post may run for a revision and then for the original post. Check the ID passed to the callback, the post type, $update, and whether the ID belongs to a revision. wp_is_post_revision() identifies a revision and returns its parent post ID when applicable.
How do you prevent an infinite loop when updating a post in save_post?
Before updating, confirm a change is genuinely needed. If it is, remove your callback from the hook, perform the update, then add the callback back. This prevents that callback from re-entering itself during the nested save while leaving other callbacks on the hook alone.
#1 Best Overall
function my_save_post_callback( $post_id, $post, $update ) {
// Ignore revisions so the callback operates on the parent post.
if ( wp_is_post_revision( $post_id ) ) {
return;
}
// Determine whether a change is actually needed before updating.
if ( ! needs_my_update( $post_id ) ) {
return;
}
remove_action( 'save_post', 'my_save_post_callback', 10 );
wp_update_post(
array(
'ID' => $post_id,
'post_status' => 'private',
)
);
add_action( 'save_post', 'my_save_post_callback', 10, 3 );
}
add_action( 'save_post', 'my_save_post_callback', 10, 3 );
This illustrates the pattern in the official WordPress guidance; it is not a tested, drop-in implementation. Replace needs_my_update() with an application-specific comparison. The example registers three accepted arguments because the callback uses the post ID, post object, and update status. When removing and restoring it, the callback identity and priority must match the original registration; otherwise remove_action() may fail. See the remove_action() reference and add_action() reference.
Should you use save_post_{post_type} instead?
Use save_post_{post_type} when the callback only applies to one post type. It prevents the callback from running for unrelated types, but it does not prevent recursion: wp_update_post() still triggers the post-type-specific save action for that post. Keep the unhook-and-restore protection if your callback updates the same post. WordPress introduced this specific hook in version 3.7.0; see its reference.
Hook order matters when other callbacks or plugins also update the post. During insertion, save_post_{post_type} runs before generic save_post, followed by wp_insert_post, according to the wp_insert_post() reference.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Debugging checklist for a repeated save_post callback
- Log each invocation. Record the post ID, post type,
$update, and the result ofwp_is_post_revision( $post_id ). The ID may be a revision rather than the original post. - Find the nested save. Search the callback and the functions it calls for
wp_update_post()or another operation that saves the same post. - Compare before writing. Check the existing and proposed values, and return if there is no real change to make.
- Prevent self-reentry. If the update is necessary, remove the exact callback at its registered priority around the nested update, then restore it.
- Scope the hook if appropriate. Use
save_post_{post_type}for a callback limited to one type, but retain recursion protection. - Use action counts as a clue, not a diagnosis.
did_action( 'save_post' )reports how many times the action has run. The Plugin Handbook’s hook guidance shows a once-only guard using that count. It can help diagnose or deliberately limit repeated execution, but it does not reveal which nested operation caused it.
What not to remove
Do not remove every callback on save_post as a routine fix. That can suppress unrelated plugin or theme behavior during the nested save. The documented approach is to unhook only the callback that would re-enter, using the same callback identity and priority with which it was registered.
Quick Recap
Best Value
Rank #4
- Keep track of everything from attendance to test scores
- Spiral bound
- Measures 8-1/2" x 11"
Rank #3
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




