Guide

WordPress memory_limit exhausted — how to detect it before it crashes the site

Your WordPress admin shows a half-rendered page, or the front end dies mid-load with "Allowed memory size exhausted." Raising the memory limit buys time but hides the cause. The fix is finding the one route or hook that actually eats the memory.

1. Problem

Once every week or two, a page on your site dies with Fatal error: Allowed memory size of 268435456 bytes exhausted. It is never the same page twice, and by the time you look, the request is long gone. A support forum thread or your host's chat agent tells you to raise memory_limit in php.ini from 256M to 512M. You do it. The fatals stop for a few weeks, then return.

Raising the limit is not wrong, but it is not the fix either. It buys headroom and moves the ceiling further away, and whatever was climbing toward the old ceiling keeps climbing toward the new one at the same rate. The fatal is the crash. The actual problem is a request somewhere on your site that allocates more memory than it should, and the fatal is just the moment that request finally lost the race. The real question is not "how do I raise the limit again" but "which route or hook is driving memory use up, and can I bound it before it gets there."

Peak memory per request is not one number, it is a distribution. Most requests on a normal site sit well under the limit: a page render, a REST call, a cron tick. A small number of requests, usually the same handful of routes every time, sit close to the ceiling: a bulk import, an unpaginated query, an image regeneration, a heavy admin report. Catching the problem before it crashes means watching that distribution, not waiting for the fatal.

This surfaces as the wp_request_peak_memory_mb signal. This guide covers finding the route that is climbing, bounding it instead of hiding it behind a higher limit, and setting up an early warning before the next fatal.

2. Impact

A memory fatal looks like a one-off crash, but the request that finally exhausted the limit was rarely the first one that came close. Something had been sitting near the ceiling for a while, and the cost of that is broader than the single failed page load.

  • Bulk operations fail partway through. A product import that fatals at row 4,200 of 6,000 does not roll back. You are left with a half-imported catalog, duplicate rows on the retry, and no clean signal of where it stopped.
  • Raising the limit costs you, quietly. A higher memory_limit often means a bigger PHP-FPM worker footprint, which on shared or fixed-RAM hosting means fewer concurrent workers or a forced upgrade to the next hosting tier, to protect against a handful of routes that were never supposed to need that much.
  • The offending route degrades before it crashes. A request approaching the ceiling is usually also slow, because allocating hundreds of megabytes of PHP objects and running garbage collection under pressure takes time. Visitors hitting that route see a sluggish page well before anyone sees a fatal.
  • The next limit raise just delays the next outage. If the underlying route is unbounded, doubling the limit buys time proportional to how much traffic or data volume grows before the next crash. A catalog that fatals a bulk export at 50,000 rows today will fatal it at 90,000 rows next quarter, at whatever limit you raised it to.

3. Why It’s Hard to Spot

WordPress does not track memory use per request anywhere you can see by default. PHP knows the peak for the request it just handled, via memory_get_peak_usage(), but nothing writes that number down unless a plugin explicitly logs it. The number exists for a few milliseconds and then it is gone.

Hosting dashboards make this worse, not better. Most show server-level or account-level memory, an aggregate across every PHP-FPM worker and every site on the box, sampled every few minutes. That tells you the server is under memory pressure in general. It cannot tell you that /wp-admin/admin-ajax.php?action=bulk_regen_thumbnails is the one route consistently sitting at 90% of the per-process limit while every other route on the same server sits at 20%.

The fatal itself only fires once the ceiling is actually crossed, so by construction you never see the climb, only the final step. A route that has been running at 240MB against a 256MB limit for months looks completely healthy until a slightly larger product catalog, a slightly bigger image, or a slightly longer session pushes it over. Nothing before that moment looked like an incident.

Uptime monitors and APM tools that sample a handful of pages on a schedule almost never catch this either, because the routes that run close to the ceiling are usually not the homepage. They are the ones your monitor does not poll: an admin report, an import endpoint, a cron-triggered job, or a rarely visited archive page with an enormous result set.

4. Cause

The wp_request_peak_memory_mb signal records the peak memory a single request allocated. It climbs when a request has to hold more data in memory than usual, or holds onto data it should have released. A small number of request shapes account for almost every case:

  • A bulk import or export with no batching. Reading a whole CSV, XML feed, or product catalog into an array before processing it holds the entire dataset in memory at once. This is the single most common cause of a memory fatal on WooCommerce and other catalog-heavy sites. Signal: wp_request_peak_memory_mb far above the site median on the import route.
  • An unbounded WP_Query. Leaving out posts_per_page, or setting it to -1, tells WordPress to load every matching post object, with all of its meta, into memory in one pass. It works fine on a small site and fatals the day the post count crosses a few thousand. Signal: wp_request_peak_memory_mb rising in step with content growth, not traffic.
  • On-the-fly image regeneration. Generating thumbnail sizes for a large original image inside the same request that is serving a page, rather than at upload time, loads the full-resolution image into memory to resize it. A single 20-megapixel upload processed inline can push a request from a normal 40MB to over 300MB. Signal: a sharp wp_request_peak_memory_mb spike tied to media-heavy routes.
  • A heavy admin or reporting page. A dashboard widget or reporting screen that aggregates all orders, all users, or all form submissions in PHP instead of pushing the aggregation to the database query itself will scale its memory use directly with row count. Signal: wp_request_peak_memory_mb elevated specifically on /wp-admin/ routes.
  • A runaway hook. A callback attached to a common action or filter, such as save_post or pre_get_posts, that accumulates state across a loop, or that recursively triggers the same hook, grows memory use with the size of whatever it is iterating over rather than with the request itself. This is the hardest to spot because the route that fatals looks ordinary; the cost is hidden inside a hook that fires on it.

In every case, the request nears the ceiling before it crosses it. That approach is what wp_memory_near_limit_total exists to catch: it increments when a request's peak memory comes within a margin of the configured limit, whether or not that particular request happened to fatal.

5. Solution

5.1 Diagnose (logs first)

Do not start by editing php.ini. Start by finding which route is actually driving the number up.

1. Find the fatal and note the exact limit it hit. The number in the error message is the configured limit at the moment of the crash, which tells you what you are working against.

grep -nE "Allowed memory size" /var/www/wp-content/debug.log | tail -n 20
grep -nE "Allowed memory size" /var/log/php-fpm/error.log | tail -n 20

A typical line names the limit and, if the request managed to log anything before dying, the file that was executing:

[14-Jul-2026 09:41:07 UTC] PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 20971520 bytes) in /wp-content/plugins/catalog-sync/importer.php on line 312

2. Confirm the configured ceiling. WordPress has its own memory constants layered on top of the PHP memory_limit, and it is easy to raise the wrong one and see no change.

// wp-config.php: WP_MEMORY_LIMIT caps front-end requests,
// WP_MAX_MEMORY_LIMIT caps wp-admin requests. Both are bounded above
// by the PHP-level memory_limit set by your host.
define( 'WP_MEMORY_LIMIT', '256M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

3. Find the route driving the number up, not just the file it died in. The line the fatal names is where the allocation finally failed, not necessarily where the memory started accumulating. Look at what the request was doing overall: was it a scheduled import, an admin screen, a REST call, a normal page render.

# Cross-reference the fatal timestamp against access logs for the same
# minute to see the route, method, and query string that was in flight
grep "14/Jul/2026:09:41" /var/log/apache2/access.log | grep -v "\.css\|\.js\|\.png"

In Logystera, wp_request_peak_memory_mb is recorded per request with the route as a label, so instead of reconstructing this from two log files you can go straight to the routes with the highest peak values and see the pattern across days, not one crash.

5.2 Root Causes

Match what the request was doing to the likely cause before changing any code.

  • Fatal fires on an import, export, or sync endpoint, often at a predictable row count each time. Cause: the whole dataset is loaded into an array before processing. Signal: wp_request_peak_memory_mb scaling with data volume on that route.
  • Fatal fires on an archive, search, or listing page, and got worse as content grew. Cause: a WP_Query with no posts_per_page limit. Signal: wp_request_peak_memory_mb trending upward over weeks, not spiking suddenly.
  • Fatal fires on upload or media-heavy routes, correlated with specific large source images. Cause: thumbnail generation running inline on the request instead of offloaded. Signal: wp_request_peak_memory_mb spiking sharply and briefly rather than trending.
  • Fatal fires only in /wp-admin/, on a dashboard widget or reporting screen. Cause: aggregation done in PHP over a full result set instead of in the database query. Signal: wp_memory_near_limit_total clustered on admin routes even when the fatal itself is rare.
  • Fatal fires on an otherwise ordinary route, with no obvious bulk operation involved. Cause: a hook callback accumulating state or recursing. Signal: wp_memory_peak_percent sitting near its ceiling on that route on every request, not just the one that fataled, which is the signature of a structural leak rather than a one-off spike.

5.3 Fix

Raising WP_MEMORY_LIMIT or the host's memory_limit can be a legitimate short-term stopgap, but it is not the fix. The actual fix bounds the request that was climbing.

Batch the bulk operation. Process imports and exports in fixed-size chunks, releasing memory between batches, instead of loading the entire dataset at once.

// Instead of loading every row into one array:
// $rows = get_all_catalog_rows(); // holds everything at once

$batch_size = 200;
$offset = 0;
do {
    $rows = get_catalog_rows( $offset, $batch_size );
    foreach ( $rows as $row ) {
        process_catalog_row( $row );
    }
    unset( $rows ); // release this batch before fetching the next
    $offset += $batch_size;
} while ( count( $rows ) === $batch_size );

Paginate the query. Add an explicit, sane posts_per_page to any WP_Query currently set to -1, and use fields => 'ids' when you only need identifiers rather than full post objects.

$query = new WP_Query( array(
    'post_type'      => 'product',
    'posts_per_page' => 100,   // never -1 on a route that scales with content
    'fields'         => 'ids', // skip loading full post objects when possible
    'no_found_rows'  => true,  // skip the COUNT(*) if you don't need pagination totals
) );

Offload image work. Move thumbnail generation to a background job or a scheduled task that runs outside the request that serves the page, or delegate it to a CDN or image service that resizes on delivery instead of on upload.

Push aggregation into the database. Replace PHP loops that sum, count, or group a full result set with a single query that does the same work in SQL, returning only the aggregate rather than every underlying row.

Fix the hook. Audit the callback for state that accumulates across iterations (arrays or objects built up inside a loop and never cleared) and for recursive triggering of the same action. Unhook and reattach with a guard flag if the same hook fires more than once per request unexpectedly.

5.4 Verify

The fix is confirmed when the signal for that route comes down and stays down, not when one manual test of the import or page load succeeds once.

# Re-run the operation that used to fatal and confirm no new fatals
grep -nE "Allowed memory size" /var/www/wp-content/debug.log \
  | awk -F']' '$1 > "[14-Jul-2026 10:00:00 UTC"'

# Trigger the fixed route directly and check it completes
curl -s -o /dev/null -w "%{http_code}\n" "https://example.com/wp-admin/admin-ajax.php?action=catalog_sync"

In the metric, watch wp_request_peak_memory_mb for that route drop and flatten across a full traffic cycle, including whatever schedule triggers the bulk operation (a nightly import, a weekly export), not just your one manual re-run. wp_memory_near_limit_total should stop incrementing for that route entirely. If wp_memory_peak_percent still sits above 0.7 on the route after the fix, the batching or pagination limit you chose is still too large for the data volume, and the fatal has only been postponed.

6. How to Catch This Early

By the time you see the fatal, the request had already been climbing toward the ceiling for a while. The point of catching this early is watching the climb, not waiting for the crash.

This issue surfaces as wp_request_peak_memory_mb.

Nothing in default WordPress tracks this per request, let alone alerts on it. PHP discards the peak-memory figure the instant the request finishes unless something records it, and the built-in error log only writes a line the moment a request actually crosses the limit. There is no built-in equivalent of "this route has been running at 85% of its memory ceiling for three weeks", even though that is exactly the state you want to know about before it becomes a fatal.

That is the gap wp_memory_near_limit_total closes. It fires when a request's peak comes within a margin of the configured limit, whether or not it goes on to fatal, which gives you the warning at the point a route starts running hot rather than the moment it finally fails. Reading peak memory as a per-route distribution and counting the near-misses as a signal is what turns "the site randomly fatals sometimes" into "route X has been trending toward its ceiling for two weeks, fix it now."

7. Related Silent Failures

  • wp_memory_peak_percent climbing: the ratio of peak memory to the configured limit, tracked per route. A route sitting at 0.85 or higher on every request, not just one, is the structural signature of a memory problem rather than a one-off spike. Covered in the memory pressure route breakdown guide.
  • wp_php_fatals_total spike: the memory fatal is one specific case of a broader class of PHP fatals. If you are seeing fatals that are not memory-related, the general fatal error guide covers reading and tracing any uncaught error or exception.
  • wp_request_peak_memory_mb correlated with slow requests: a request approaching the memory ceiling is usually also a slow request, because large allocations and garbage collection under pressure both cost time. If your complaint started as "the site feels slow" rather than a fatal, the slow page load guide is the better starting point.
  • wp_memory_near_limit_total with no preceding warning: unlike a plugin conflict or a contract break, a memory fatal usually has no wp_php_warnings_total precursor naming a symbol. The fatal causal chain guide explains how to tell a resource fatal apart from a code-contract fatal when the log line alone is ambiguous.

See what's actually happening in your WordPress system

Connect your site. Logystera starts monitoring within minutes.

Copyright © 2026 Logystera. Operated by 1969730 Ontario Inc., an Ontario, Canada corporation. All rights reserved.