The error arrives as a single line that says less than it appears to: Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes), followed by a file path and a line number. The path is usually somewhere in wp-includes or a plugin folder, and it moves depending on which upload or which screen tripped it.
Both numbers are worth reading. 134217728 bytes is 128M, the ceiling PHP was enforcing at that moment. 20480 bytes is 20KB, the allocation that got refused. The 20KB is not the culprit. Something before it had already consumed 128 megabytes, and the next small allocation happened to be the one that met the wall.
The standard response is to open wp-config.php, add define( 'WP_MEMORY_LIMIT', '512M' );, reload, and get the identical error with the identical 134217728 in it. That happens because WordPress has three separate memory numbers, they apply in different situations, and the one most people edit sits underneath the one that actually stopped the request.
Three ceilings, not one
The first ceiling is PHP’s own memory_limit, set in php.ini, a PHP-FPM pool config, or a host control panel. This is the number quoted in the fatal error, and it is the only one PHP enforces. Everything WordPress does on top of it is a request to change it.
WP_MEMORY_LIMIT is WordPress’s baseline request, and it applies to every request including front-end page loads. Core defaults it to 40M on a single site and 64M on multisite. During bootstrap it compares that value against the current PHP limit and calls ini_set( 'memory_limit', WP_MEMORY_LIMIT ) only when the constant is higher. It never lowers the limit, so a server already running at 256M is not dragged down to 40M.
WP_MAX_MEMORY_LIMIT is the higher ceiling WordPress asks for when it knows the work is heavy. It defaults to 256M, or to the php.ini value when that is already larger. It is not applied on every request; it is applied when something calls wp_raise_memory_limit(). wp-admin/admin.php calls it with the admin context on every admin screen, and both image editor classes call it with the image context when they load a file for resizing. Each context has its own filter, admin_memory_limit and image_memory_limit, so a plugin can adjust one without touching the constants. WP-Cron got its own cron_memory_limit context in WordPress 6.3.
The word doing the work in all of that is request. Both constants act through ini_set(). On many hosts the call succeeds, because memory_limit is one of the settings PHP allows a script to change at runtime. On plenty of others it does nothing, because the host has locked the value, disabled ini_set, or enforces the real ceiling outside PHP entirely: a container limit, a cgroup, an FPM pool cap. Core checks with wp_is_ini_value_changeable( 'memory_limit' ), and when the answer is no it stops guessing and defines both constants as the current PHP value.
That is the entire explanation for “I raised the limit and nothing changed”. Defining WP_MEMORY_LIMIT as 512M on a server that caps you at 128M and refuses runtime changes gives you a constant holding the string 512M and a PHP process that still dies at 134217728 bytes. The constant is not lying. It is a request nobody granted.
Seeing all three at once
Before editing anything, read the numbers. Tools → Site Health → Info has a Server section listing the PHP memory limit, and a WordPress Constants section listing WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT. One detail there is easy to walk past: when the limit in force on admin screens differs from the server’s, core adds a second row labelled “PHP memory limit (only for admin screens)”. That row appearing is proof the ini_set() call worked. Its absence means the two values are the same, which is either a generous server or a locked one.
For a live reading inside a real request, this notice prints all three plus what the request has used so far. It belongs in a small site-specific plugin or in your theme’s functions.php file, and it should come back out once you have the answer.
add_action( 'admin_notices', function () {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
echo '<div class="notice notice-info"><p>'
. 'PHP memory_limit: ' . esc_html( ini_get( 'memory_limit' ) )
. ' | WP_MEMORY_LIMIT: ' . esc_html( WP_MEMORY_LIMIT )
. ' | WP_MAX_MEMORY_LIMIT: ' . esc_html( WP_MAX_MEMORY_LIMIT )
. ' | peak so far: ' . esc_html( number_format( memory_get_peak_usage( true ) / 1048576, 1 ) ) . ' MB'
. '</p></div>';
} );
Two things about that reading. memory_get_peak_usage( true ) with the true argument reports real memory allocated from the system rather than the smaller figure PHP tracks for its internal bookkeeping, and the real figure is the one memory_limit is measured against. And because the notice renders inside the admin, the memory_limit it prints is whatever is in force on admin screens, normally WP_MAX_MEMORY_LIMIT rather than the server baseline. Site Health is where the baseline is.
The notice also reads the peak at the moment it renders, which misses everything that happens later in the request. To catch the real peak, log it at shutdown instead, with both WP_DEBUG and WP_DEBUG_LOG set to true so the line lands in wp-content/debug.log. On a live site set WP_DEBUG_DISPLAY to false at the same time.
add_action( 'shutdown', function () {
$peak = number_format( memory_get_peak_usage( true ) / 1048576, 1 );
$uri = isset( $_SERVER['REQUEST_URI'] )
? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) )
: 'cli';
error_log( sprintf( 'peak %s MB of %s on %s', $peak, ini_get( 'memory_limit' ), $uri ) );
} );
Run an upload, a bulk regeneration, a slow admin screen, then read the log. It records the requests that finish, which is the point: ordinary front-end page loads sit well under the limit while one specific admin operation peaks several times higher, and that gap tells you whether you have a ceiling problem or a single routine doing too much.
If you have WP-CLI, the three numbers are one command:
wp eval 'echo ini_get("memory_limit") . " | " . WP_MEMORY_LIMIT . " | " . WP_MAX_MEMORY_LIMIT . PHP_EOL;'
Treat that reading with suspicion. WP-CLI runs under the CLI build of PHP, which frequently has a different memory_limit from the one serving web requests, sometimes none at all. A CLI reading of 512M is no evidence about what the browser-facing pool allows.
Why images trigger it first
A JPEG is compressed on disk and uncompressed in memory. The file size tells you almost nothing about what it costs to open.
Take a 4000 x 3000 photo, the ordinary output of a phone. That is 12 million pixels, and GD holds each pixel of a truecolor image as a 32-bit value, so the bitmap alone is roughly 48 million bytes, about 46MB, before any of PHP’s own overhead. On disk that same photo might be a 5MB JPEG. The ratio is close to ten to one, and it worsens with dimensions: 8000 x 6000 is four times the pixels and lands near 180MB as a bitmap.
Now add the resize. Producing a 300 x 300 thumbnail means the source bitmap and the destination bitmap are resident at the same time. The source is not released afterwards, because core builds every subsequent size from the same loaded original: make_subsize() resizes, saves, frees the copy, and returns to the original for the next one. The floor for handling that upload is therefore around 46MB held for the whole of metadata generation, no matter how small the outputs are.
Then multiply. Every additional crop a theme or plugin registers is another resize inside the same request, and a commercial theme plus a gallery plugin plus WooCommerce pushes past a dozen entries without trying. Checking how many image sizes your site actually registers is worth doing before blaming the limit, because bulk regeneration across all of them is the most memory-hungry thing a normal WordPress site ever does. A single upload survives it. Regenerating a few thousand attachments does not.
Two details make this less tidy than the arithmetic suggests. Since WordPress 5.3, core scales down originals whose longest side is over 2560 pixels and keeps a -scaled copy, but producing that scaled file still means loading the full original first, so the peak is unchanged. And if your server uses Imagick rather than GD, the pixel data lives in ImageMagick’s own cache, which PHP’s memory_limit does not count. Imagick sites tend to fail with a different message, or with no PHP message at all, while memory_limit looks perfectly healthy.
The three ceilings are easier to reason about with the numbers filled in, and the error message itself carries two of them.
Paste the line from your log below. It converts the byte figures into megabytes, names the file and the plugin they came from, and then works out which of the three limits is actually in force in the front end, in the admin and during an upload, which is the part that surprises people. There is a calculator underneath for the other half of the question, how much a given image will need before it has even finished resizing.
Memory limit calculator
Paste the fatal error line from your debug.log and this reads the numbers out of it, then works out which of the three memory limits is really in force in the front end, in wp-admin and while uploading. Everything is worked out in this browser tab, nothing is sent anywhere.
1. Read the error line
Paste the whole line, or a few lines around it. The first line that mentions an exhausted memory size is the one that gets read.
| Limit that ran out | not read yet |
|---|---|
| Tried to allocate | not read yet |
| File | not read yet |
| Line | not read yet |
| Where that file lives | not read yet |
A small figure under "tried to allocate" does not name the culprit. That allocation was only the last drop into a bucket that was already full, and PHP reports the drop, not the filling. What filled it is what you are looking for: an import, a query without a limit, an image being resized, a loop over thousands of posts.
2. The three limits, and which one applies where
There is not one memory limit, there are three, and they apply in different places.
| Setting | Where it applies | Default |
|---|---|---|
| memory_limit | PHP itself, on every request. The hard ceiling: WordPress can ask for more, but only the host decides whether PHP is allowed to go there. | set by the host, often 128M or 256M |
| WP_MEMORY_LIMIT | The front end, on every normal page view. WordPress raises the limit to this value early in the request. | 40M, 64M on a multisite |
| WP_MAX_MEMORY_LIMIT | wp-admin, uploading and image editing, where the headroom is actually needed. | 256M |
WordPress only ever raises a limit, it never lowers one. A constant below the value already in force does nothing at all. A constant above what the host allows does nothing either: PHP stays where the host caps it.
Whole numbers with K, M or G, as PHP reads them: 256M. A plain byte count works too, and -1 in the first field means no limit. Leave the last field empty unless you know your host pins memory_limit down. Leave a WordPress field empty and its default is used.
| Context | In force | Set by |
|---|---|---|
| Front end, a normal page view | 128 MB | memory_limit |
| wp-admin, dashboard and editors | 256 MB | WP_MAX_MEMORY_LIMIT |
| Uploading and image editing | 256 MB | WP_MAX_MEMORY_LIMIT |
3. Where the memory goes
Rough figures, switched on and off, so you can see which item dominates. Every number is named and can be changed.
That fits.
These are estimates, not measurements. A plugin can sit at 0.5 MB or at 15 MB, and an import loop can eat more than everything else together. Use the figures to see which item dominates, then measure the real thing with Query Monitor or a profiler before you trust a number.
4. The lines you need
Exactly the lines, with the values from the fields above. Take the one that matches how your host runs PHP.
memory_limit = 128M
memory_limit = 128M
php_value memory_limit 128M
Under PHP-FPM, which is what most hosts run today, this line does nothing at best and answers every request with a 500 at worst. Use .user.ini there.
define( 'WP_MEMORY_LIMIT', '40M' ); define( 'WP_MAX_MEMORY_LIMIT', '256M' );
Some hosts pin memory_limit down where no file of yours can reach it. Then none of the lines above changes anything, phpinfo keeps showing the old value, and only a ticket to the host helps. Ask for the value in megabytes and ask whether it applies to the web server and to WP-CLI alike.
The other usual suspects
Images dominate, but three other shapes account for most of the rest.
- Importing a large WXR file. The importer parses the export and holds post structures in memory before writing them, and a 60MB XML file does not become 60MB of PHP arrays, it becomes considerably more. Splitting the export into several smaller files works better than any constant.
- A plugin loading an entire result set at once.
'posts_per_page' => -1is harmless on a site with 200 posts and fatal on one with 40,000, because every row becomes a post object and core then primes the meta cache for all of them. The same shape turns up in order exports, CSV generators and rebuild-index routines. - Something serialising an enormous structure. Backup plugins building a full file manifest, page builders storing a whole layout tree in postmeta, analytics plugins writing large autoloaded options. Autoloaded options deserve their own look, since every one of them is read on every request, front end included, which usually shows up as a site that feels slow rather than one that is broken.
Raising it properly, or not raising it
The order matters more than the values.
- Measure first. Find the actual peak of the request that fails and compare it to the PHP limit. A request peaking just over 128M needs a different answer from one peaking at 190MB.
- Confirm the value can move at all, using the Site Health rows or the notice above. If nothing has been raised anywhere, no
wp-config.phpedit will change that, so skip to step four. - Set the constants in
wp-config.php. - Ask the host. On managed WordPress hosting the pool limit is a support ticket rather than a file you own, and one message beats an afternoon of experiments.
- If the host will not move, reduce the work instead of raising the ceiling.
The constants go anywhere above the line that begins /* That's all, stop editing in wp-config.php, because core reads them while loading and anything below that line arrives too late:
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );
On PHP-FPM and CGI setups you can often raise the underlying PHP value with a .user.ini file in the WordPress root, holding a single line. PHP caches those files for user_ini.cache_ttl seconds, 300 by default, so give it five minutes before deciding it failed:
memory_limit = 256M
You will also find advice to add php_value memory_limit to .htaccess. That directive only works when PHP runs as an Apache module, which is now the minority case, and on an FPM host it produces an immediate 500 error across the whole site. Check how your server runs PHP before trying it.
Reducing the work is often the better engineering answer anyway. Regenerate thumbnails in batches rather than in one pass, or one size at a time. Resize enormous source files before uploading them: dropping a 6000 x 4000 original to 2560 pixels wide cuts the bitmap cost by more than half and costs nothing visually, since core was going to scale it anyway. In long-running loops, query with 'fields' => 'ids' and load objects one at a time. And keep in mind that a 1G limit on a small server is not free: each PHP worker can now claim a gigabyte, so fewer of them fit, and a machine that used to go slow under load starts refusing connections instead.
When something looks wrong
A blank page with no error text. A fatal error with display_errors off produces nothing at all in the browser. The message is still in the PHP error log or in wp-content/debug.log, and the white screen of death is worth reading up on separately, because half of debugging a memory error is getting the error to show itself in the first place.
The byte count in the error never changes. Whatever you define, PHP keeps reporting the same allowed size. The ini_set() call is being refused, so the ceiling is set outside your reach. Confirm in Site Health and take it to the host.
No PHP error at all, just a 502 or a truncated response. The process was killed from outside, by the OS out-of-memory killer, a container limit, or the FPM process manager. PHP never got the chance to report anything, so raising memory_limit makes this worse rather than better.
The admin works but the front end fatals, or the reverse. That is the two constants doing exactly what they are designed to do. Admin screens run under WP_MAX_MEMORY_LIMIT, front-end requests under WP_MEMORY_LIMIT, and a plugin running heavy code in both places fails on the lower one first.
Some large uploads work and others of similar file size do not. Memory cost tracks pixel count, not kilobytes. A heavily compressed 2MB JPEG at 6000 x 4000 costs far more to open than a lightly compressed 8MB JPEG at 2000 x 1500.
Where this leaves you
The memory limit is not one setting with one place to change it. It is a server-enforced ceiling, plus two WordPress constants that politely ask for more and get told no on a meaningful share of shared hosting. Working out which of the three stopped a given request is most of the job, and Site Health plus one peak-usage reading gets you there in a few minutes.
Once the real numbers are in front of you, the decision usually makes itself. A request peaking a little over the line needs a modest raise. A request peaking at three times the limit is doing too much in one pass, and no ceiling you can realistically buy will fix it; that is a batching problem wearing a memory error as a costume.
And if images are what tipped you over, the arithmetic is the thing to carry away. Four bytes per pixel, source and target resident together, repeated once per registered size. Shrink the originals, drop the sizes nobody uses, and the ceiling stops mattering.