Engineering

Running a PHP profiler on shared hosting, without root

Everyone knows .user.ini cannot load a PHP extension. Far fewer people notice that on most shared hosts you also get your own php.ini — and that one can.

21 September 2026 · Adam Buhl

If you are on shared hosting and your store gets slow, the tools that would tell you why are all built for someone with root. New Relic wants a daemon. Tideways and Blackfire want an extension installed by the host plus an agent process. Query Monitor shows you the page you are looking at, while logged in as an admin, which is never the request that was slow. So the usual advice is: open a ticket and hope.

That advice is more pessimistic than it needs to be, and until this morning our own documentation repeated it.

The distinction that matters

Two statements about PHP configuration look similar and are not:

You get such a file in cPanel (MultiPHP INI Editor), in DirectAdmin with CloudLinux (Select PHP Version → Options), in Plesk (PHP Settings), and on plenty of smaller hosts simply as a php.ini in your home directory. Once you have it, you can point extension= at an absolute path in your own home. You never touch the provider's extension_dir.

The recipe, verified as uid 1000

This is not a thought experiment. Run in a clean container as an ordinary user with no sudo available anywhere:

uid=1000(klient) gid=1000(klient) groups=1000(klient)
8.3.33 ext: 0.15.2

{"ts":1789995109,"uid":1000,"duration_ms":20.196,"level":"full","php_ver":"8.3.33"}

🔦 PHPRay Top — (1 requests in last 600s)
200        20.2ms full

Extension loaded from a home directory, a trace written to a home directory, and the collector reading it back — all as the same unprivileged user. The steps:

mkdir -p ~/lib ~/phpray-data
curl -fsSL -o ~/lib/phpray.so \
  "https://phpray.dev/dl/$(curl -fsS https://phpray.dev/dl/LATEST)/phpray-8.3-glibc-amd64.so"

Then in the php.ini your host lets you edit:

extension=/home/YOURUSER/lib/phpray.so
phpray.master_switch=1
phpray.enabled=1
phpray.output_mode=file
phpray.output_path=/home/YOURUSER/phpray-data/traces.jsonl
phpray.mode=smart

Confirm with a phpinfo() page, then read your own traces:

curl -fsSL -o ~/phpray "https://phpray.dev/dl/$(curl -fsS https://phpray.dev/dl/LATEST)/phpray-collector-linux-amd64"
chmod +x ~/phpray
~/phpray top -f ~/phpray-data/traces.jsonl

No root. No compiler. No daemon owned by the system. No permission from anyone.

Where it will not work

A promise without limits is worth less than no promise, so here are the four ways this fails, all of which we have hit:

And one honest limitation that is not a failure: a trace file in your home directory is per-account. You see your own sites, which is exactly what you asked for, and nothing about your neighbours — that separation is deliberate and it is also how the extension behaves when a host installs it fleet-wide, with one 0600 shared-memory ring per uid.

If you cannot load an extension at all

The WordPress plugin is pure PHP and needs nothing but WordPress. It hooks $wpdb, the HTTP API and the error handler, and writes the same trace format into wp-content/uploads. It sees the application layer only — not the PHP core, not C-extension internals — and the documentation says so plainly rather than pretending the two are equivalent.

Try it on your own account

The full recipe with the per-host details is in the docs. The console demo runs on live traffic and needs no signup.

Read the guide Open the demo