The WordPress Menu Item Limit or: Help! Half my menu items just disappeared!

Many an unsuspecting WordPress user has fallen victim to the menu item limit effect. Here I’ll attempt to explain why it happens, how to detect it before it becomes an issue, and how to properly configure the server to avoid limiting your menu items.

Written by

Chris Mavricos

Published on

April 13, 2013
BlogWordPress

So, you just saved your WordPress 3 menu and found to your horror that you lost menu items – maybe a LOT of menu items. And now you can’t add any more – when you add a new one, the one last drops off the end of the list.

Is this a WordPress bug? Did one of those darn plugins do it?! Nope, it’s a server configuration issue that silently cuts your menu items down before WordPress even knows about them, like wolves picking off sheep in the night. Or you know, the HTTP/PHP equivalent of that.

Shake your fist at the sky and take a deep breath, and I’ll lay it out for you.

High Level Overview

Let’s start at the beginning. There are three important things to understand:

1. To save a WordPress menu, you have to send data from the client (your web browser) to the server, where it is processed and stored by the WordPress Menu System. This form data is submitted to the server via an HTTP POST. PHP parses this data as a nice array that we know and love – $_POST.

2. The WordPress Menu System bases the menu items it saves on the data it receives from the client side via the $_POST array. It assumes that data that is not present should be removed – that is, WordPress deletes menu items not present in the $_POST array upon save.

3. Depending on your server configuration, not all of the POSTed variables sent by the client may be received by the WordPress, as the $_POST array can be truncated. And when that data is lost, we lose menu items. Dang.

Hold on, I’ve had enough of the techno-babble. Skip to the Solution

Here’s how it works

Every menu item on the Appearance > Menus screen has about 11-12 fields associated with it (Title, Classes, Description, ID, etc). There are also a few meta values for the menu overall (like the menu name, menu ID, and a few nonce-related fields for security). All this data gets sent to the server as POST data. Each field is a separate POST variable. So if you have 10 menu items, you’re sending around 120 POST variables (11 per menu item + various menu meta fields). If you have 100 menu items, you’re sending around 1200 POST variables.

Here’s the problem

Unfortunately, PHP’s desire to be secure and WordPress’s desire to be tidy conspire to stab you in the back. Two wrongs might not make a right, but in this case, two rights make a big ol’ headache.

PHP’s Security

For security reasons, PHP likes to limit the maximum numbers of POST vars you can submit. This is to mitigate the effects of DoS attacks. We’ll get into details below. The important bit from the PHP manual:

If there are more input variables than specified by this directive, an E_WARNING is issued, and further input variables are truncated from the request.

This is good, but in practice, if you’re unaware of this setting (and aren’t paying attention to Warnings in the logs – and the average user isn’t), it can produce stress-induced manual hair extraction.

WordPress’s housekeeping

The WordPress Menu System save mechanism works by iterating over the $_POST array, and then discarding any menu items that weren’t present in the array.

//nav-menus.php line 341
// Remove menu items from the menu that weren't in $_POST
if ( ! empty( $menu_items ) ) {
	foreach ( array_keys( $menu_items ) as $menu_item_id ) {
		if ( is_nav_menu_item( $menu_item_id ) ) {
			wp_delete_post( $menu_item_id );
		}
	}
}

That’s good, as it prevents us from having all sorts of orphaned menu items in the posts table. But it also means that if the menu system is sent incomplete data, WordPress assumes the menu items corresponding to that data should be deleted, rather than that they should be ignored.

The Upshot: The road to confounding data loss is paved with good intentions

In the end, if the client submits more menu item variables than the PHP-configured limit, any menu items over the limit will be lost. So if you submit 1200 variables, and the limit is 1000, the last 200 are simply discarded – WordPress never even knows they were sent. These 200 variables will correspond to the last 18 items or so in your menu.

And that is why the menu items are lost. PHP prevents the menu items that you saved on the client side from reaching WordPress’s processing on the server side, so WordPress decides to delete the menu items. Who’s at fault? (I demand blood!) No one, really. It’s just a situation where you need to configure your server properly.

Sidenote: If this happens, it’s all over. We haven’t just lost pointers, we’ve lost the actual data. The only way to recover the menu items would be to restore a backup of the database prior to the menu save. Gotta catch it before it happens to avoid the repercussions.

Menu Item Specifics

Here’s how the POST variables that are submitted on menu save break down, for those who want specifics:

Key

Abbreviation Example Value
n 100 Number of Menu Items
c 10 Number of Custom Menu Items
m 2 Number of Registered Theme Locations

POST Variables (Example values using 100 menu items)

Key Variable Count Example Count Explanation
menu-name 1 1 User-defined Menu Name
save_menu 1 1 Submit button
closedpostboxesnonce 1 1 nonce
meta-box-order-nonce 1 1 nonce
update-nav-menu-nonce 1 1 nonce
_wp_http_referer 1 1 nonce
action 1 1 Form meta
menu 1 1 Menu ID
menu-item-url c 10 Link URL for custom menu items
menu-item-title n 100 Item Title
menu-item-attr-title n 100 Item Title Attribute
menu-item-classes n 100 Item Custom Classes
menu-item-xfn n 100 Item XFN Attribute (rel=)
menu-item-description n 100 Item Description
menu-item-db-id n 100 Menu Item ID
menu-item-object-id n 100 Linked object ID
menu-item-object n 100 Linked object type (post/page/custom)
menu-item-parent-id n 100 Item Parent ID
menu-item-position n 100 Item Position
menu-item-type n 100 Item Type
menu-locations m 2 Theme Location
Total 1120

You can estimate about 12 variables per menu item to be safe. So if you want to save a menu with 150 items, you’d want a POST variable limit of no less than 150*12 + 10 = 1810.

Sidenote: the actual number varies because some menu item types have more fields than others. For example, custom menu items have an extra field to set a URL as they are not linked to a post object.

So why does this happen suddenly and without warning?

Sometimes this issue manifests as a user reaching a simple limit: they’ve added 50 menu items. They try to add the 51st, and nothing happens if they’re adding it at the end. Or if they add it in the middle, the last menu item is dropped. The limit is 50, so every time they add X more items, the last X are deleted.

Much more distressing is the case where a user has happily had 120 menu items for the last month. Then, one fateful day, they go to manage their menu, as they have new content they wish to add. Upon adding their 121st menu item and saving, they find that suddenly, to their horror, the last 40 menu items have been deleted. How could that happen? The POST vars limit should have prevented them from adding 120 menu items in the first place, right?

The most likely scenario leading to this massive frustration is that the server’s PHP version has been updated, either by the user or the web host (or possibly added Suhosin, but let’s focus on one thing at a time). And here’s why:

The PHP directive max_input_vars was introduced in PHP 5.3.9 (a relatively recent addition from January 10, 2012). The real issue is that this directive has a default value of 1000. That’s a limit of around 80 menu items. The problem comes from this sequence of events:

  1. User adds 120 menu items while host is at PHP version < 5.3.9 (no menu item limit, because max_post_vars does not exist)
  2. Host upgrades server PHP to 5.3.9+
  3. User adds 121st menu item. Only the first 80 are sent to the server due to new POST variable limit. User loses 40ish menu items
  4. User rips out hair and smashes computer against wall. Vows to quit WordPress once and for all
Sidenote: I don’t actually recommend approaching 100 menu items, even in a mega menu. The general rule of thumb is that having more than 100 links on a page can have a negative impact on SEO. Plus, presenting a user with 100 options is generally overkill, and you should probably be rethinking the navigation on your site to make it more user-friendly

How UberMenu factors in (for those who care)

UPDATE for UberMenu 3

As of UberMenu 3.0, UberMenu’s settings have been extracted from the standard menu system save process and are now saved via AJAX in a single serialized string, so UberMenu’s settings no longer contribute to this limit. The limit will be the same whether or not UberMenu is present.

UberMenu 2 (this section does not apply to UberMenu 3)

I’d like to quickly address the impact my plugin, UberMenu – WordPress Mega Menu Plugin, has on this scenario. While UberMenu does not cause this limit, it does cause the user to reach their menu item limit faster, because it adds one extra variable to each menu item.

Note that UberMenu 2 actually adds around 10 extra settings to each menu item. However, in an effort to minimize this limitation effect, UberMenu serializes all of its settings into a single active form field (via javascript), so that it only contributes one additional POST variable per menu item, rather than 10 per item. This optimization was added in UberMenu 2.0.

So here’s how it breaks down with some quick examples.

max_input_vars Normal Menu Item Limit (approx) UberMenu 2 Item Limit (approx)
1000 82 76
2000 165 153
3000 249 230

Adding UberMenu 2 to the mix means the limit decreases by about 6 menu items per 1000 POST vars.

Unfortunately, this has the effect of customers believing UberMenu is the cause of the issue, because with UberMenu enabled they can only add 76 menu items; with UberMenu disabled, they can add 77, 78, 79… and as such it is natural to confuse symptom for cause and conclude that UberMenu is the problem. It’s not; it just makes the problem apparent sooner, unfortunately.

Server Configuration Solution

As with most problems, once you understand the issue, it’s easy to solve it. The server configuration is limiting the number of POST variables the can be submitted to the server; we need to increase that.

There are two ways this limit may be imposed

1. PHP’s max_input_vars

The increasingly common issue is the max_input_vars PHP directive. By default, it is set to 1000. To increase this, simply set it in php.ini. I’d suggest 3000 to be safe, but 2000 should be sufficient for most sites.

max_input_vars = 3000

How do you edit php.ini? That depends on your host. If you have access to the php.ini file, simply add or edit the directive, save, and restart Apache. If you’re with a web host that doesn’t give you access (common with shared hosting), you may have to contact your host and have them make the change for you.

2. Suhosin

Prior to PHP’s max_input_vars, the Suhosin PHP Security module introduced a similar pair of max_vars directives that are identical in purpose. Not all servers run Suhosin, but those that do will need to increase the following directives in php.ini:

suhosin.post.max_vars = 3000
suhosin.request.max_vars = 3000

Again, you may need to contact your host to get them to make these changes if you don’t know how or can’t do it yourself.

Restarting Apache with the new maximum POST variable values should allow you to submit all of your menu item variables and therefore save new menu items. Hoorah!

Keep in mind this won’t actually recover your lost menu items – that data is gone. The only way to get it back would be to restore from a backup copy of your database.

How to edit php.ini on various hosts:

Is there a better solution?

Wouldn’t it be nice if this wasn’t an issue? Theoretically, there may be a few options. In practice, most are more easily said than done.

Option 1: Condense all of the WordPress Menu Item fields into a single input before submitting

This is what I did with UberMenu – just serialize all the settings and decode the string on the server side. It works well, with two caveats. First, we still have the potential to reach a menu item limit eventually. Second, it means we don’t have a non-javascript fallback, and for core WordPress functionality, that’s a no-go. So that solution is probably out.

Option 2: Save menu items individually

We could save menu items via AJAX, individually, so we’d only ever submit data for one menu item at a time. Again, this requires javascript, and I suspect it would also require a major reworking of how the menu system works. It would also have to be implemented carefully in order to be intuitive and convenient; I think there would still need to be a Save-All button that would sequence all of the menu items for processing.

I don’t have enough expertise in the nitty gritty of the menu system to say whether this is truly a viable/practical option.

Option 3: Use a “checksum” to protect from unintended deletions

I’m using the term loosely here, but we could submit an extra form field that acts as a “checksum“, indicating how many menu items are being sent to the server for processing. WordPress’s menu save process could then check this value to make sure the number of menu items that it has received in the $_POST array is equivalent to the number of items it expects to receive. If it finds a discrepancy, it could avoid deleting missing menu items; however, it may be too late to save newly submitted menu items that were previously unknown.

Option 4: Native Nested Menus

Another thought would be to provide a native nesting/import functionality in WordPress. Essentially, each menu item would have an option to nest a separate menu as its submenu. This would allow users to divide up their menu items among multiple menu trees, to be dynamically assembled when the menu is printed. There are some existing plugin solutions out there that already do this, I believe. This would also enhance the reusability of the menu system, as users could potentially use the same menu subtree fragments in multiple menus.

Note that UberMenu 3 has a feature called “Menu Segments” that allows you to do this

Option 5: Provide a Warning

I think this is really the best option, at least in the short term. Simply detect the potential issue and alert the user to it. Providing explanations and solutions (or links to solutions) within the interface is generally the most efficient way to inform a user of what they need to be aware of and resolve.

And that brings us to…

The Menu Item Limit Detector Plugin

As a first step toward this, I’ve written a rudimentary plugin that attempts to detect impending menu item limits by roughly (and conservatively) estimating the remaining POST variables based on the prior save and the current variable limits. It adds an alert in the Appearance > Menus screen when the user approaches the limit, and explains the issue.

Menu Item Limit Warning

Note that I’ve already included a very similar piece of code within my UberMenu plugin to detect this scenario for UberMenu customers. Available in UberMenu 2.3+

The obvious downside to the plugin is that users need to be aware of the issue in order to install it in the first place… and the whole point of the plugin is to provide that awareness. Chicken and egg much? This’d be far more useful in WordPress Core.

Download the Plugin Contribute on Github

Please try out the plugin and let me know if it helps! If it’s useful, I’ll add it to the WordPress plugin repository.

And let me know if you have any creative suggestions for solving this problem! I highly doubt the options I’ve presented are comprehensive, but I hope they start the conversation going in the right direction.

Chris Mavricos

Hi, I'm Chris. I'm a web developer and founder of SevenSpark located in Boulder, CO. I've been developing websites since 2004, and have extensive experience in WordPress theme, plugin, and custom development over the last 15+ years.