Contents
- 1 Why memory management in PHP quietly shapes everything we ship
- 2 The mental model: PHP’s short life and long leaks
- 3 How PHP sees memory: zvals, copy-on-write, and your arrays
- 4 memory_limit: not an enemy, a boundary
- 5 The hidden gravity of arrays
- 6 Objects vs arrays: memory and meaning
- 7 Long-running scripts: where memory mistakes go to multiply
- 8 Diagnosing PHP memory issues: watching the invisible
- 9 Generators: streaming instead of hoarding
- 10 The emotional side: memory leaks at 3 AM
- 11 For those hiring and those being hired
- 12 A few grounded habits that keep PHP memory sane
- 13 The quiet craft behind PHP work
Why memory management in PHP quietly shapes everything we ship
There’s a particular kind of silence in the office (or your kitchen) around 11:30 PM.
The tests are green.
The feature works.
But the server is dying.
You open the logs and see it:
Allowed memory size of 134217728 bytes exhausted…
You sigh. Coffee is cold. The bug report is vague.
And yet, this is one of the most honest messages PHP ever gives us:
“I did exactly what you asked. You just asked for too much.”
Friends, let’s talk about that “too much.”
Not from the angle of “just increase memory_limit and move on,” but from the calmer, deeper side: what is PHP memory management actually doing under the hood, and how can we treat it with a bit more respect?
Because if you work with PHP long enough—Laravel, Symfony, WordPress, legacy piles, or shiny greenfield—the way your code uses memory is the difference between:
- an API that hums along at 50ms and sleeps peacefully at night, or
- a job queue worker that slowly bloats until it gets killed, resurrected, and killed again like some cursed cron zombie.
And if you hire PHP developers on Find PHP, or you’re looking for a new PHP job yourself, memory awareness is one of those quiet skills that separate “it works” from “it lasts.”
The mental model: PHP’s short life and long leaks
Most of the time, PHP lives a short, dramatic life.
Request comes in → PHP script starts → it does its work → sends response → dies.
Process ends, memory is freed. Simple. Clean. That’s why for years many of us didn’t think too hard about memory management in PHP.
But then:
- long-running CLI workers appeared,
- queue systems like Laravel Horizon and Symfony Messenger became normal,
- WebSockets, daemons, ReactPHP, RoadRunner, Swoole, Amp, timers, schedulers, stateful apps.
Suddenly, memory stopped being “someone else’s problem.”
Here’s a simple mental model:
- For classic HTTP requests, PHP’s memory is like a disposable notepad. You scribble, you throw it away.
- For workers and daemons, PHP’s memory is like a whiteboard that never gets fully erased. Old drawings fade, but ghosts remain—especially if you forget to clean up.
If you remember nothing else from this article, remember this:
In long-running PHP, every small memory mistake is cumulative.
That’s where understanding the basics of memory management stops being academic and starts being survival.
How PHP sees memory: zvals, copy-on-write, and your arrays
Inside PHP, there’s a small data structure that secretly controls your life: the zval.
You don’t need to memorize its fields. But you should know what it represents:
every variable you create—array, int, string, object—is wrapped in a zval that carries:
- the value type (string, array, object, etc.),
- the value itself (or a pointer to it),
- a reference count (how many variables are pointing to this value),
- some flags (is it a reference, etc.).
This is where one of PHP’s most misunderstood behaviors comes in: copy-on-write.
Take this:
$a = [1, 2, 3];
$b = $a;
$b[] = 4;
You might think $a is copied when you assign $b = $a;. In reality, initially, they share the same underlying array.
$aand$bpoint to the same zval, with a reference count of 2.- When you modify
$b, PHP detects a write and creates a copy of the array for$bonly. - Now two arrays exist in memory.
This is efficient, but it has consequences:
- Copying large arrays or strings is cheap until you write to them.
- Innocent-looking operations can trigger massive hidden copies.
When you do something like:
function appendValue(array $data, $value): array
{
$data[] = $value;
return $data;
}
$result = appendValue($bigArray, 42);
You might be copying a giant array just to add one value. On a small script that runs once, who cares. On a worker that processes thousands of messages per hour, this can build up.
At scale, behavioral patterns like this matter more than individual lines of code.
memory_limit: not an enemy, a boundary
The memory_limit in php.ini (or in .htaccess or ini_set) is PHP’s way of saying,
“I refuse to burn the house down.”
You’ll see it in this shape:
Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes)
Two numbers here:
- the limit (e.g. 134217728 bytes = 128 MB), and
- the extra it tried (20 KB in this case).
There are usually three categories of reaction to this error:
- junior: “Let’s double
memory_limit.” - mid: “Where is it leaking?”
- senior: “Why is this process doing this much in one run at all?”
The truth is: all three may be valid at times—but that last one is where system-level thinking lives.
You don’t fix a cargo problem by making the truck infinitely bigger.
Sometimes you ship less per trip. Sometimes you split the journey. Sometimes you move computation around.
memory_limit is there as a safety net. If you keep raising it and never investigate, you’re just building a taller cliff to fall from.
PHP developers love arrays the way some people love spreadsheets.
Config arrays. Result arrays. DTO-ish arrays. Temporary arrays for mapping and transforming and grouping and caching and “we’ll refactor this later” arrays.
But arrays are not free. Especially nested arrays.
Imagine:
$rows = $pdo->query('SELECT * FROM big_table')->fetchAll(PDO::FETCH_ASSOC);
It works. It always works. Until one day “big_table” becomes actually big.
- Every row is an array.
- Every column name is a string.
- Every value is a zval.
- All stored at the same time in memory.
Then your CLI import script starts dying on production only. During peak hours. Of course.
The alternatives are not rocket science, but they require intention:
- use
PDO::FETCH_NUMwhen you don’t need associative keys, - stream results with
while ($row = $stmt->fetch(...)) { ... }instead offetchAll, - process in batches instead of loading everything at once,
- for queues, commit work in smaller chunks and let the worker breathe.
The principle behind all this:
“Keep the live working set of data as small as possible.”
You don’t carry the entire library to read one book.
Don’t carry the entire table to process one row.
Objects vs arrays: memory and meaning
Another question that quietly affects memory:
Do you model your data as arrays or as objects?
From a pure memory perspective:
- Arrays are flexible but heavy, especially when used as pseudo-objects.
- Objects have some overhead but can encode intent and structure.
This is where performance meets code clarity.
A small, focused object:
class UserProfile
{
public function __construct(
public int $id,
public string $email,
public ?string $name,
) {}
}
can sometimes be more memory-efficient than passing around giant associative arrays that carry tons of unused fields… especially if those fields trigger more processing elsewhere.
But the deeper win is not just memory. It’s meaning.
When your code expresses intent more clearly, you make better decisions about:
- when to load data,
- when to discard it,
- what truly needs to live in memory at the same time.
Memory management is not only about unset() and clever tricks.
It’s largely about design.
Long-running scripts: where memory mistakes go to multiply
If you work only with classic request-response PHP, your memory problems are transient.
But if you’re operating in environments like:
- Laravel Horizon workers,
- Symfony Messenger consumers,
- custom daemons, command bus handlers,
- WebSocket servers, Swoole/RoadRunner setups,
you’re living in a different world.
Look at something like this:
while (true) {
$job = $queue->reserve();
handleJob($job);
}
In theory, fine. In practice, several subtle things can go wrong:
- global state keeps growing,
- static caches never reset,
- external libraries accumulate references,
- event dispatchers keep listeners attached,
- closures capture more than needed.
Even tiny leaks—1 KB per job—turn into megabytes over time.
Some practical habits here:
- Prefer stateless services inside workers; inject dependencies but avoid storing job-specific state on the worker object itself.
- Periodically restart workers (most queue systems support “max jobs” or “max memory”).
- Avoid unbounded internal caches; put hard limits or TTLs.
- Be careful with libraries that keep static registries, caches, or singletons.
Sometimes the most honest memory strategy is:
“Let it do N jobs, then restart it cleanly.”
It’s not cowardice. It’s engineering.
Diagnosing PHP memory issues: watching the invisible
Debugging memory is frustrating because you can’t see it directly.
But PHP gives us some tools that, combined with a bit of discipline, go a long way.
One very humble but underrated tool:
echo memory_get_usage(true);
memory_get_usage()tells you how much memory the script is using right now.memory_get_usage(true)includes extra information about the system allocation.
Paired with memory_get_peak_usage() you can answer:
- “Did this loop increase memory line by line?”
- “Did it all spike at one operation?”
- “Is memory freed after each iteration, or is it accumulating?”
Example idea:
$start = memory_get_usage(true);
foreach ($items as $i => $item) {
process($item);
if ($i % 1000 === 0) {
echo "After $i items: " . memory_get_usage(true) . PHP_EOL;
}
}
echo "Peak: " . memory_get_peak_usage(true) . PHP_EOL;
It’s crude, noisy, and wonderfully honest.
On larger projects, tools and profilers (Xdebug, Blackfire, Tideways, etc.) can give you deeper insight into what functions or allocations are eating memory. But even then, the principles remain:
- reduce data held at once,
- shorten the lifetime of big variables,
- break work into smaller units,
- simplify object graphs.
You can’t reason well about performance in PHP without occasionally looking at memory like this.
It’s like developing in the dark and forgetting you own a lamp.
Generators: streaming instead of hoarding
One of the most elegant tools PHP gave us in recent years is the generator (yield).
Instead of returning a gigantic array:
function getAllRows(): array
{
$rows = [];
while ($row = fetch()) {
$rows[] = $row;
}
return $rows;
}
You can do:
function rows(): iterable
{
while ($row = fetch()) {
yield $row;
}
}
Now the caller can iterate row by row, and only one (or a few) rows live in memory at a time:
foreach (rows() as $row) {
process($row);
}
This pattern is a game changer when:
- importing millions of records,
- exporting large CSV files,
- streaming API responses,
- working with large external APIs.
It shifts your thinking from “return everything” to “produce as needed.”
That mindset is one of the most powerful weapons against memory issues.
The emotional side: memory leaks at 3 AM
Let’s be honest.
No one dreams of “learning PHP memory management.”
It rarely appears in course curriculums or job listings. It’s not glamorous.
But you remember the night your worker crashed repeatedly, and the business team was watching.
You remember that Slack message from ops: “We’re hitting memory limits again.”
You remember scrolling through code, eyes dry, trying to see invisible patterns.
There’s a moment—every developer who sticks around long enough hits it—where you realize:
This isn’t just about code. It’s about care.
Care for the system.
Care for the people who depend on it.
Care that says, “I won’t just make it pass tests. I’ll make it last in production.”
Memory management in PHP is part of that care.
It’s invisible to users, but its absence is spectacular when everything falls down.
For those hiring and those being hired
If you’re using a platform like Find PHP to hire PHP developers, you’re not just buying syntax knowledge.
You’re looking for:
- developers who can reason about data size, concurrency, and memory boundaries,
- engineers who understand that a job worker processing 1,000 items and 1,000,000 items are fundamentally different problems,
- people who ask “What’s the expected volume?” before they write the code.
You can hear this mindset between the lines in an interview:
- “How did you handle imports on large datasets?”
- “What did you do to keep queue workers stable over time?”
- “Have you ever debugged a memory leak in PHP? What did you learn from it?”
And if you’re on the other side of the table, building your PHP resume or profile:
-
Don’t just write “worked with queues.”
Say, “Designed a long-running Laravel queue worker with controlled memory footprint and periodic restarts.” -
Don’t just write “built APIs.”
Say, “Implemented streaming responses and chunked processing to handle large datasets under constrained memory.”
It tells a story about how you think.
That story matters more than whether you remember a specific function name.
A few grounded habits that keep PHP memory sane
No magic. Just habits I’ve seen make a difference in real codebases:
- Avoid
fetchAll()on large datasets. Stream withfetch()and process incrementally. - Use generators for any large sequences you don’t need all at once.
- Break heavy jobs into smaller jobs—let the queue distribute and isolate memory usage.
- Watch your caches. In-memory cache is nice until it becomes a silent hoarder. Put limits.
- Reset between iterations in long-running workers: clear arrays, close resources, unset large variables.
- Measure. Use
memory_get_usage()and simple logging to see how memory behaves over time. - Respect
memory_limit. Raise it when appropriate, but always ask why you needed to.
These are small, almost boring practices.
But they’re exactly the kind of boring that keeps systems alive.
The quiet craft behind PHP work
What I love about PHP is not that it’s perfect. It isn’t.
What I love is that it has grown up with us. From tiny scripts to serious platforms.
From “include this file and echo” to orchestrating services, queues, complex domains, and global products.
Memory is part of that growing up.
It’s not just something for C developers or low-level engineers.
If you care about how your PHP code behaves after deployment—for weeks, months, years—then memory is part of your craft.
You don’t need to become obsessed. You don’t need to memorize every internal detail.
But you can build a quiet sense of responsibility:
- I will think about how much data I load.
- I will think about how long objects live.
- I will think about what happens when this job runs not once, but a million times.
And somewhere, late at night, when the logs are calm and the workers are stable and you’re closing your laptop with that small, satisfying tired smile—
you’ll know this invisible discipline is part of the reason things are still running.
That feeling, more than anything else, is why it’s worth understanding how PHP holds your data, and how gently you can learn to let it go.