WordPress Menu Not Saving Items — Why Entries Vanish When You Click Save Menu
Save Menu deletes every item that is missing from the submitted form, and it skips the Trash. Find out why the items never reached the server: a script that did not run, a PHP input limit, a blank label or a request the server threw away.
When a WordPress menu loses items on save, WordPress deleted them: Save Menu removes every item that was not in the submitted form, and it skips the Trash. So the question is never "why didn't it save" but "why didn't those items reach the server". There are four answers, and each one leaves a different trace.
What Save Menu actually does
The Menus screen posts the whole menu form back to nav-menus.php, which checks the update-nav_menu nonce and then calls wp_nav_menu_update_menu_items(). That function:
- Loads every item currently in the menu from the database.
- Loops through
$_POST['menu-item-db-id']and updates each item it finds, skipping any item whose Navigation Label is empty. - Deletes every item it did not update with
wp_delete_post().
That last step is the one that hurts. wp_delete_post() only sends posts and pages to the Trash. Menu items are nav_menu_item posts, so they are deleted permanently.
The famous culprit is PHP's max_input_vars limit, 1,000 by default. Each menu item submits about a dozen fields, so around 75 items is enough to exceed it, and PHP silently drops every variable past the limit. WordPress has a guard for this: on submit, nav-menu.js serializes the entire form into one hidden field, nav-menu-data, and _wp_expand_nav_menu_post_data() unpacks it back into $_POST on the server. That field sits at the top of the form, so it survives truncation and restores everything. On a current install, max_input_vars only bites when that guard did not run.
Match the symptom
| What you see after Save Menu | Likely cause |
|---|---|
| The last items of a long menu are gone | nav-menu-data was empty, so PHP truncated the raw fields |
| Only the item you just edited is gone | Its Navigation Label was blank |
| The page reloads, no "has been updated" notice, nothing changed | The request exceeded post_max_size, so PHP discarded the whole body and no save ran |
| A 403 page or a blank screen | A firewall rule rejected the request body |
| The admin is right but the site shows the old menu | Caching or a theme location problem, not saving — see menu not showing |
Check whether the script ran
Open the browser's developer tools on Appearance › Menus, switch to the Network tab and click Save Menu. Select the nav-menus.php request and look at the form data:
nav-menu-dataholds a long JSON string → the guard worked; look at server limits and firewalls instead.nav-menu-datais empty → the submit handler never ran. Switch to the Console tab: a JavaScript error from a plugin or theme script loaded on the Menus screen stopsnav-menu.jsbefore it attaches the handler. Deactivate plugins that add menu fields or mega-menu options until the error disappears.
Plugins that replace the menu item editor with their own markup can also break the serialization without a visible error. If the payload is empty with a clean console, test with those plugins off.
Check the PHP limits
Look in the PHP error log (enable debug logging if you have no other) for:
PHP Warning: PHP Request Startup: Input variables exceeded 1000. To increase the limit change max_input_vars in php.ini. in Unknown on line 0
and for POST Content-Length of … bytes exceeds the limit of … bytes, which is the post_max_size case. The Menus form is sent as multipart data, so you may also see "Multipart body parts limit exceeded"; that limit follows max_input_vars unless max_multipart_body_parts is set. Then read the live values:
wp eval 'echo ini_get("max_input_vars"), " ", ini_get("post_max_size"), PHP_EOL;'
Run that command through the same PHP version the site uses; the CLI can read a different php.ini than the web server.
Raise the limits in php.ini, a .user.ini in the site root, or the hosting panel:
max_input_vars = 3000
post_max_size = 64M
Here is the trap: ini_set( 'max_input_vars', 3000 ) in wp-config.php does nothing. PHP parses the request body before WordPress loads, and the setting cannot be changed at runtime. Many guides show that line; it fails without an error. Page builders hit the same limit for the same reason, which is why Elementor sections lose fields on low limits.
Blank labels delete items
If you clear an item's Navigation Label, the list shows "(no label)", and on save wp_nav_menu_update_menu_items() skips the item, then deletes it as unsubmitted. Nothing warns you. Give every item a label, even when you hide the text with CSS for an icon-only link.
Firewalls and request size
A serialized menu with a few hundred items is a large request body full of URLs. Web application firewalls inspect that body and may block it as suspicious or as too large. The result is a 403 or the host's block page. Ask for the rule ID in the firewall log and an exception for /wp-admin/nav-menus.php, rather than turning the firewall off.
Recover deleted items
There is no undo. Restore wp_posts, wp_postmeta and wp_term_relationships from a backup to a staging copy and rebuild from it, or recreate the items by hand. Before you work on a large menu again, take a snapshot you can rebuild from:
wp menu item list main-menu --fields=db_id,title,link,menu_item_parent --format=csv > menu-backup.csv
For very large menus in a classic theme, Appearance › Customize › Menus is a useful fallback: it saves through the Customizer's changeset, a single JSON value. Block themes do not use this screen for their header at all; their navigation lives in the Navigation block, described in how to create a WordPress menu.
Common mistakes
- Raising
max_input_varswithini_set()and assuming it worked. - Saving again and again after items vanish, which deletes more each time the cause is still present.
- Waiting for an error such as the link you followed has expired when
post_max_sizeis exceeded: on the Menus screen the page just reloads unchanged. - Clearing labels to make icon-only items.
Verify
- The Network tab shows
nav-menu-datafilled on every save. ini_get()returns the new limits from the web server, not only the CLI.wp menu item listreturns the same number of items before and after a save.- The PHP log has no new "Input variables exceeded" lines.
Frequently asked
- No. PHP parses the request into $_POST before any script runs, so ini_set() in wp-config.php comes too late, and max_input_vars cannot be changed at runtime anyway. Set it in php.ini, a .user.ini file in the site root, or your hosting panel's PHP settings, then confirm the new value with ini_get().
- No. Menu items are a post type of their own, and wp_delete_post() only sends posts and pages to the Trash. Items removed by a save are deleted from the database straight away. The only way back is a database backup or rebuilding them.
- In classic themes, Appearance › Customize › Menus saves through the Customizer's changeset, which travels as one JSON value rather than a dozen fields per item. It is a practical workaround for a very large menu while you fix the server limits.