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.
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:
.user.inicannot load an extension.extension=isPHP_INI_SYSTEM, and per-directory files are read long after the engine has started its modules. This is whyphp_value phpray.enabled=1works in.user.inibutextension=silently does nothing.- Your own
php.iniis system level — for your own PHP processes. Nothing aboutPHP_INI_SYSTEMsays "the host's file". It means the ini that PHP reads at startup, and on shared hosting that is frequently a file you control.
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:
open_basedirconfines PHP topublic_html. PHP then cannot read~/lib/phpray.so. Moving the file inside the allowed tree usually fixes it.- The host gives you no writable
php.ini, or filtersextension=out of it. Nothing to do from the account side. - The build does not match your PHP. Version, thread safety (use the NTS builds) and libc all have to line up. PHP refuses the module at startup and names the API number it expected.
- Each request runs in a fresh container without your home. Rare on classic shared hosting, normal on PaaS.
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