# PHP Isolation and Limits

> What a PHP script on a CorePanel server can and cannot do: open_basedir, the account's private temporary directory, disabled functions, process limits, and what an account can change with a .user.ini.

Source: https://www.corepanel.net/docs/php/isolation-and-limits/
Last updated: 2026-08-27
Part of the CorePanel documentation — https://www.corepanel.net/docs

---

[How PHP Runs](https://www.corepanel.net/docs/php/how-php-runs) covers *who* executes PHP — the account's own
system user, in its own pool. This page covers what that process is allowed to do once it
is running, and what a site owner can change.

Everything here is written into the account's pool file by CorePanel, and re-applied to
existing accounts when `corepanel-sys` starts. You do not configure it per account; you
inherit it.

## Where PHP can read and write

`open_basedir` confines every file operation to the account's own trees:

| Path | What it is |
|---|---|
| `~/public_html` | The primary domain's document root, shared with its aliases |
| `~/domains` | One directory per addon domain and subdomain |
| `~/tmp` | The account's private temporary directory (below) |

Everything else in the home directory is **outside** PHP's reach — `~/mail`,
`~/backups`, `~/.ssh` — and so is the rest of the filesystem. A script that tries dies
with *open_basedir restriction in effect*, which is the message to look for when a plugin
that works elsewhere fails here.

> **Imported accounts get extra entries**
>
> A cPanel account configured by hand can serve a site from a path in neither tree — say
> `~/dl.example.com`. [The importer](https://www.corepanel.net/docs/cpanel-import) records those and adds them to
> `open_basedir` **one path at a time**. They are never merged into a wider prefix: opening
> up the whole home directory would put the account's mail, backups and SSH keys inside
> PHP's reach to save a line of configuration.
## The private temporary directory

`/tmp` is shared by every account on the server. CorePanel does not let PHP use it.

Instead each account gets `~/tmp` — mode `0700`, owned by the account — and all four
settings that decide where PHP puts temporary files point at it:

| Setting | Covers |
|---|---|
| `sys_temp_dir` | `sys_get_temp_dir()`, `tempnam()`, `tmpfile()` |
| `upload_tmp_dir` | Multipart file uploads in flight |
| `session.save_path` | Session files |
| `open_basedir` | Excludes `/tmp` entirely, so nothing can reach it |

The fourth is what makes the other three complete. Setting only `upload_tmp_dir` and
`session.save_path` — the usual advice — leaves `sys_get_temp_dir()` pointing at the
shared `/tmp`, and that path is used by Composer, PHPMailer, image libraries and a long
tail of plugins. On a shared server that means one account reading another's leftovers,
and, worse, a script `include()`-ing a file another account planted there.

The [command-line shim](https://www.corepanel.net/docs/php/how-php-runs#php-on-the-command-line) applies the same
setting, so a cron job and a web request agree about where temporary files live.

## Disabled functions

The pool disables the functions that exist to run other programs:

```
exec  passthru  shell_exec  system  proc_open  popen  show_source
```

Well-behaved web software does not need any of them, and the list is what stops a
compromised site from turning a PHP bug into a shell.

> **The CLI is not covered by this list**
>
> `disable_functions` is a **pool** setting, so it applies to web requests. PHP invoked from
> the command line — a [cron job](https://www.corepanel.net/docs/accounts/cron-jobs), or SSH — uses the version's
> own `php.ini`, where these functions are available.
>
> That is not an oversight: an account owner running their own cron already controls what
> runs as their user, so restricting the CLI would be theatre. It does mean the boundary is
> "a visitor cannot get a shell", not "this account can never run one" — and it means an
> application that needs `exec` should be moved to cron **as an operator's decision about
> that account**, not as a way for a customer to route around the pool.
## Process limits

| Setting | Value | What it means |
|---|---|---|
| `pm` | `ondemand` | No worker exists until a request arrives |
| `pm.max_children` | 8 | At most 8 concurrent PHP requests for this account |
| `pm.process_idle_timeout` | 10s | An idle worker exits |
| `pm.max_requests` | 500 | A worker is recycled after 500 requests, capping any leak |
| `rlimit_files` | 4096 | Descriptor ceiling per worker |

`pm.max_children` is the one that shows up in practice. It is a per-account ceiling, so a
site being hammered queues its own requests instead of taking the server down with it —
but it also means a genuinely busy site can hit 8 concurrent PHP requests and start
queuing. If a site is slow and its `php-error.log` is quiet, this is worth checking
before anything else.

`rlimit_files` exists because workers inherit the master's descriptor limit, which
CorePanel raises to 65536. Without a cap, one account's descriptor leak would starve
every other account sharing that PHP version's master.

## Per-request limits

Each individual request also has ceilings, so one bad script cannot hold resources
forever. Unlike everything else on this page, **these are yours to change** — in the panel
under **Resource limits → Per request**, or from the command line:

```bash
corepanel php limits                              # what is in effect
corepanel php limits set --memory-limit 512       # only what you name changes
```

![The Per request page of the Resource limits section, introduced by the line "What a single PHP request may take. These bound one request, not one account: what an account can hold at once is this memory limit multiplied by how many requests it may run in parallel." Below it the PHP limits card shows the seven values on an untouched server: memory limit 256 MB and kill stuck requests after 300s both marked with a padlock, max execution time 120s, max input time 120s, max input variables 3000, max upload size 64 MB and max post size 64 MB. A note says these are CorePanel defaults and that pools which already set a value of their own keep it. The footer reads "Worst case per account: 8 concurrent requests x 256 MB = 2048 MB" beside an "Apply to every account" button](https://www.corepanel.net/_astro/php-limits-dark.DuNfZ8eS.png)

| Setting | Default | Can the site change it? |
|---|---|---|
| `memory_limit` | 256M | **No** — pinned |
| `request_terminate_timeout` | 300s | **No** — pinned |
| `max_execution_time` | 120 | Yes, with a [`.user.ini`](https://www.corepanel.net/docs/php/user-ini) |
| `max_input_time` | 120 | Yes |
| `max_input_vars` | 3000 | Yes |
| `upload_max_filesize` | 64M | Yes |
| `post_max_size` | 64M | Yes |

The padlock marks the two a site cannot raise for itself. The line above the button is
the one to read before typing a bigger number: these are per *request*, so what the
server has to survive is `pm.max_children` of them at once.

They are server-wide, and only a super-administrator can see or change them: a reseller
setting them would be reaching into every other reseller's accounts.

Two combinations are refused, because each produces a server that is quietly wrong rather
than one that reports an error:

- The kill timeout must stay **above** `max_execution_time`. Below it, php-fpm ends the
  worker before PHP can write anything, and every runaway script becomes a 502 with an
  empty error log.
- `post_max_size` must be at least `upload_max_filesize`. PHP checks the POST size first,
  so a larger upload limit could never be reached.

`request_terminate_timeout` is the important one, and it is worth understanding why it
exists next to `max_execution_time`. On Linux, `max_execution_time` **does not advance
while PHP is waiting on the system** — a request blocked on an API that never answers,
or a database query that never returns, can sit there forever without ever tripping it,
holding one of the account's 8 workers the whole time. Eight of those and the site
returns nothing but errors. `request_terminate_timeout` is what ends them.

It is deliberately set well above `max_execution_time` so that PHP is normally what stops
a runaway script — PHP writes a *Maximum execution time exceeded* line to the error log,
which tells you what happened, whereas php-fpm can only kill the worker and leave you
with a 502 and no explanation.

> **There is still no per-account CPU or memory cap**
>
> These are per *request*, not per account: the worst case is `pm.max_children` requests at
> `memory_limit` each. A single account running expensive scripts can still make the server
> slow for everyone — it can no longer take the machine down with it (see the server-wide
> ceiling below), but nothing yet divides the budget fairly between accounts. Watching
> [the dashboard](https://www.corepanel.net/docs/dashboard) is the tool for that today.
> **Existing servers get these on upgrade, and saving changes the rule**
>
> Until you save these once, the limits are a **floor**: they are added to accounts that
> already exist when `corepanel-sys` next starts, but only where the pool does not set them
> already. A value you raised by hand in a pool file is left exactly as it is.
>
> The first save changes that. From then on every CorePanel-managed pool is rewritten to
> match what the panel shows — including pools edited by hand, because a settings page
> displaying a number the server is not using is worse than either value. If an account
> genuinely needs its own limit, raise the server-wide one rather than editing its pool.
## The server-wide ceiling

The limits above bound one request. They do not bound the *sum* of them, and the sum is
what takes a server down: enough accounts busy at the same time, and PHP alone can use
every byte of RAM on the machine — at which point the kernel's OOM killer starts choosing
victims, and it does not know that MariaDB and Dovecot matter more than a worker.

So all of PHP runs inside one systemd slice, `corepanel-php.slice`, with a ceiling of its
own. Every installed PHP version's master (`php81-php-fpm`, `php83-php-fpm`, …) is placed
in it, which is the point: a limit written once per version would multiply by however
many versions happen to be installed.

| Limit | How it is sized | On a 4 GB / 4-core server |
|---|---|---|
| `MemoryMax` | Everything except a reserve of 1 GB, or 25% of RAM, whichever is larger | 3072M |
| `MemoryHigh` | 80% of `MemoryMax` — the kernel reclaims here, before it kills | 2457M |
| `CPUQuota` | Every core but half of one | 350% |
| `TasksMax` | Flat backstop against a fork bomb coming out of PHP | 8192 |

![The Server ceiling page of the Resource limits section, titled "All of PHP together" and subtitled "corepanel-php.slice · 4 GB / 4 cores". Three panels read: Memory, 709 MB of 3 GB with a progress bar and the note "Reclaim starts at 2.4 GB"; CPU quota, 350%, explained as "100% is one core. The half core PHP never gets is what keeps SSH and the panel answering under a runaway loop"; and Processes, 61 of 8192, described as a backstop against a fork bomb. Below, a "Choosing your own numbers" card gives the drop-in path /etc/systemd/system/corepanel-php.slice.d/override.conf](https://www.corepanel.net/_astro/resources-server-ceiling-dark.Cb2bnAJv.png)

The reserve is what the rest of the machine keeps: sshd, the panel, MariaDB, Postfix and
Dovecot. It has an absolute floor of 1 GB because a percentage alone would leave a small
VPS without enough room to log in and fix things.

The CPU quota is half a core the server never gives PHP, and it buys exactly one thing:
under a runaway loop, that half core is the difference between *the panel is slow* and *I
cannot ssh in to see why*.

Masters also run with `OOMPolicy=continue`, so if the kernel does kill a PHP worker for
memory, systemd lets the master carry on. The default is to stop the service — which
would turn one runaway request into every site on that PHP version going down at once.

Seeing what it is doing:

```bash
systemctl show corepanel-php.slice -p MemoryMax -p CPUQuotaPerSecUSec
systemd-cgtop corepanel.slice          # live memory and CPU for all of PHP
```

> **Sizing happens when corepanel-sys starts**
>
> The numbers are derived from the host's RAM and core count each time `corepanel-sys`
> starts, so after resizing a VPS, restart it — `systemctl restart corepanel-sys` — and the
> ceiling follows the new machine. The first start after upgrading also restarts each
> php-fpm master once, because systemd cannot move a running service into a slice.
>
> The slice file is managed by CorePanel and rewritten on every start. To set your own
> numbers, add a drop-in instead — `/etc/systemd/system/corepanel-php.slice.d/override.conf`
> — which wins over the generated file and is never touched.
On EL8 the kernel runs cgroups v1, where `MemoryHigh` does not exist; the hard `MemoryMax`
still applies, so those servers get the stop without the gentler slowdown first.

## Limiting one account

The two ceilings above bound a request and the machine. Neither divides the machine
*between* accounts: one busy site can fill the whole PHP budget on its own, and the
others get whatever is left.

**Resource limits → Per account** is where that is decided. The number is
how many PHP requests one account may run **at the same time**. Past it, the account's
next request is answered `508 Resource Limit Is Reached` — the same status code
CloudLinux uses, so existing monitoring and support articles apply unchanged — while
every other account on the server is untouched.

![The Per account page of the Resource limits section. A card titled "Concurrent PHP requests per account" explains the limit and offers a Default limit field reading "No limit", an Apply button and a badge reading "Not configured". Under it, a table of accounts — acme, blogco and shop — with columns Account, Memory, CPU, Procs, PHP now, Peak, 508s and Limit: shop is using 1 GB, 123% CPU and 14 processes with 6 PHP requests in flight against a peak of 8, and every account's limit reads None with a pencil button to change it](https://www.corepanel.net/_astro/resources-usage-dark.DxySmeFa.png)

That is what makes the per-request memory limit into a per-account bound:

```
RAM an account's PHP can hold  ≤  concurrent requests × memory_limit
```

With the shipping defaults, an account limited to 4 requests can hold at most
4 × 256 MB = 1 GB, enforced at the door instead of by a cgroup.

> **Nothing is limited until you set it**
>
> A fresh install — and any server that upgrades into this — has **no limit at all**.
> The panel shows *Not configured*, no account is capped, and the server behaves exactly
> as it did before. CorePanel does not pick a number for you, because starting to refuse
> requests on a working server is not something an upgrade should do quietly.
Two values mean different things and the panel keeps them apart:

| Value | On the server | On one account |
|---|---|---|
| *(empty)* | No limit at all, nothing is written | Follow the server default |
| `0` | Unlimited, on the record | Exempt this account from a server that limits the others |

The same page lists what each account is **actually** using — memory, CPU, processes,
requests in flight, the peak, and how many were refused. Memory and CPU are read from
`/proc` and cover everything the account runs (PHP workers, its cron jobs, an SSH
session), not only PHP.

> **A dash is not a zero**
>
> Those columns come from two services, and either can be unavailable on its own. A figure
> that could not be read shows as `—`, with the reason above the table. An account using
> nothing and an account nobody could ask about must not look alike on a page you suspend
> accounts from.
>
> CPU is a rate between two samples, so it reads `0%` for everyone until the second one.
Lowering a limit never kills what is already running: those requests finish, and the new
ceiling applies to what arrives next.

From the command line, the same thing is [`corepanel resources`](https://www.corepanel.net/docs/cli#corepanel-resources).

## What an account can change

Everything above is written into the pool with `php_admin_value`, which a site cannot
override. What is left — five of the seven per-request limits, and most of the everyday
PHP settings — a site owner sets for themselves with a **`.user.ini`** file in the
document root, which PHP-FPM reads natively:

```ini
; ~/public_html/.user.ini
upload_max_filesize = 128M
post_max_size = 128M
max_execution_time = 300
max_input_vars = 5000
```

That is the supported route, and it has enough detail of its own to deserve a page:
where the file goes for each kind of domain, which directives it can and cannot set, and
why an edit takes five minutes to appear. See
[Changing PHP Settings with `.user.ini`](https://www.corepanel.net/docs/php/user-ini).

Two things worth knowing here, because they are the ones people discover the hard way:

- **`memory_limit` is not on the list.** A `.user.ini` cannot raise it, because it is what
  bounds how much memory the account's PHP can hold. A script that needs more stops with
  *Allowed memory size exhausted*, and raising it is the server operator's call —
  server-wide, under **Resource limits → Per request**, not per site.
- **It is not `.htaccess`.** `php_value` lines in an [`.htaccess`](https://www.corepanel.net/docs/web/htaccess) are
  a mod_php feature; PHP-FPM does not read them, here or on any other FPM host.

An operator with root can, of course, change anything. For the seven per-request limits,
do it in the panel or with `corepanel php limits set`: that is where they live, it reaches
every account in one step, and it survives the pool rewrite. For anything else CorePanel
does not expose, use a `.ini` in the PHP version's `php.d` directory rather than editing a
pool by hand — CorePanel rewrites the settings it owns on service start, so a hand edit
there can be normalized away, while `php.d` is yours and is never touched.
