
A memory leak looks like steady, one-directional growth in used memory over hours or days that never comes back down, even during quiet periods with no traffic. That is the distinguishing feature: normal memory usage rises and falls with load — busy during peak hours, lower overnight — while a leak keeps climbing regardless of what the server is doing. The most reliable tell in practice is behavioral, not numerical: if a server or a specific service "needs" a periodic manual restart to feel fast again, or if an ops team has quietly started restarting PHP-FPM or a Node process every night as a matter of routine, that routine restart is a leak being managed rather than fixed. A single high memory reading from free -h or top proves nothing on its own — it could just be a busy moment, or the kernel doing its job by using spare RAM for disk cache. What proves a leak is the trend line: the same measurement taken repeatedly, hours apart, showing a number that only ever goes up.
Confirm a suspected leak by sampling the same metric on a timer and watching it over at least a few hours, not by looking once. If you already have a suspect process ID, a simple loop does the job without installing anything:
Redirect that loop's output to a file (append >> /var/log/memcheck.log) and let it run through a normal business day. If you don't yet have a suspect process, drop free -h >> /var/log/memtrend.log into a cron job that fires every five minutes, then read the log back after a few hours and look at whether the "used" column trends upward independent of traffic, or whether "available" keeps shrinking while nothing else on the server changed. A single snapshot answers "how much memory is used right now" — only a series of snapshots answers "is this actually a leak."
Rank every process by resident memory with ps aux --sort=-%rss | head — the top rows are your suspects, sorted highest to lowest. This is usually enough to spot an obvious offender: a single PHP-FPM worker, a Node process, or a background daemon sitting at the top of the list and visibly larger than its siblings. The one weakness of plain ps is that it double-counts shared memory across processes — if twenty PHP-FPM workers share the same loaded libraries, ps reports each worker's share as if it were fully private, which can make memory usage look worse than it is and makes it harder to compare processes fairly. smem fixes this by calculating proportional set size, splitting shared memory fairly across the processes using it:
Run smem a few times over the same window you used for confirmation. A process that is merely large (a database buffer pool, a big Node heap doing legitimate caching) will hold roughly steady. A process that is leaking will show its own RSS or PSS climbing on every pass while everything else around it stays flat.
A single PHP-FPM worker whose memory grows steadily across hundreds of requests, rather than resetting between them, is the classic PHP leak signature — usually caused by a poorly written loop that accumulates data in a static or global variable, a buggy extension, or a large object (an image resource, a big array from a database query) that never gets explicitly freed within the request lifecycle. PHP's per-request memory model is supposed to reclaim everything when a request ends, but persistent data structures, opcode cache misbehavior, or C-extension bugs can leak around that boundary anyway.
The standard mitigation lives directly in the pool configuration: pm.max_requests tells FPM to kill and respawn a worker after it has served a fixed number of requests, which caps how large a leaking worker can grow before it's replaced with a clean one. On a CloudStick server, every website gets its own isolated PHP-FPM pool file — for example /etc/php83cs/fpm-pools.d/<sitename>.conf — provisioned with pm = ondemand and pm.max_requests = 500 by default:
That single directive means a leaking worker never has more than 500 requests to grow before it is recycled — the leak resets to a clean baseline on a schedule instead of accumulating without bound until the server runs out of RAM. Lowering it further (to 200 or even 100) is a completely legitimate short-term stopgap while the actual leaking code is being found and fixed; it costs a small amount of process-respawn overhead in exchange for a hard ceiling on worst-case memory growth.
Editing an FPM pool file requires root or sudo access to the server, and any change to pm.max_requests needs a pool reload (systemctl reload php83cs-fpm, adjusting the service name for your installed PHP version) before it takes effect on new workers.
Recycling a worker treats the symptom; finding the actual leaking code requires looking inside the process itself, and the right tool depends on the language. For Node.js applications, start the process with the built-in inspector — node --inspect app.js — and connect Chrome DevTools' Memory panel to it remotely to capture heap snapshots at two points in time, then use the "comparison" view to see exactly which object types are accumulating between snapshots. The heapdump npm package is the equivalent tool for production processes you can't easily attach a debugger to: trigger a dump programmatically or on a signal, then load the resulting .heapsnapshot file into Chrome DevTools offline for the same comparison.
For PHP, there is no equivalent heap snapshot workflow in most stacks, so the practical approach is bisection with memory_get_usage(): sprinkle calls to it (and memory_get_usage(true) for the allocated-from-system figure) before and after suspect blocks of code, log the delta, and narrow down which function or loop iteration is responsible for the growth. This is tedious but reliable — it turns "the whole request leaks somewhere" into "this specific function call adds 40MB and never releases it," which is usually enough to spot the missing unset(), the unbounded array, or the resource handle that was never closed.
The fastest way to catch a leak before it becomes an outage is to be watching the trend line continuously rather than waiting for the server to fall over. CloudStick's Server Stats panel graphs memory usage over time on every plan, so a slow leak shows up as a visibly rising line days before it ever becomes a crisis — you don't need to set up your own monitoring stack just to see it coming. For background workers or queue processors that are known to leak and are waiting on a code fix, Supervisord Management (available on the BASIC plan and above) lets you configure an automatic restart schedule for that specific process, which achieves the same effect as the PHP-FPM pm.max_requests trick but for any long-running worker, not just PHP.
As a concrete next step: pull up your Server Stats graph right now, look at the memory line over the last seven days, and if it has a staircase shape rather than a saw-tooth one, start the confirmation loop from earlier in this article on whichever process is largest.

