Profiling Slow PHP Requests with Xdebug
Knowledgebase Article
}
Knowledgebase Article
Modern servers are quick. A fresh WordPress or Magento install on the hosting account you're using right now would load in well under a second - no cache, no tricks, just a clean install on a modern CPU. But your site doesn't. Maybe pages take five seconds to render. Maybe your CPU usage is suspiciously high for the amount of traffic you're getting. Maybe the public side feels fine but the admin grinds, or everything's fine right up until your LiteSpeed cache expires and the next visitor pays the rendering bill.
The hardware isn't the problem. Something in the site is - something that wasn't there when it was new. Plugins added over the years, a custom theme that's been worked on by three different developers, a page builder with its own ecosystem of add-ons, a WooCommerce installation with five years of accumulated extensions. Somewhere in there, some piece of code is doing far more work than it should be on every page load. The question is which piece.
The traditional answer is "turn things off until it gets fast again." Deactivate a plugin, refresh. Deactivate another, refresh. Switch to a default theme, refresh. This works fine if you have five plugins and a simple theme. It works less well if you have eighty plugins, a custom theme, and a checkout that breaks the moment you disable the wrong thing. And critically, it tells you which plugin is the issue, not why - or whether the real cause is something subtler interacting between several of them.
Profiling is the proper way to do this. Instead of guessing what's slow, you ask PHP to record exactly what it spent time doing during a single request - every function called, how long it took, and what called it. The output is a cachegrind file, and it's the difference between "the site feels slow" and "this specific function consumed 7.3 seconds of a 10.5-second page load, and here's the line of code that triggered it."
This article walks through the full profiling workflow on Kualo hosting: how to get Xdebug enabled on your account, how to capture a profile, and how to make sense of the resulting cachegrind file - either using a free utility called QCachegrind, by handing it to an AI assistant like Claude, or by passing it to us for analysis as a paid service.
Profiling is the right tool when "turn things off until it gets fast" stops working. In practice, that's most of the time:
header.php is making 14,000 database calls, the conversation moves on quickly.We strongly recommend running profiling against a staging copy of your site rather than production. There are three good reasons:
If staging and production share the same cPanel account (and therefore the same PHP configuration), enabling Xdebug in PHP Selector will affect both - but the points above still hold.
Each step is described in detail below.
Open a support ticket and ask us to enable Xdebug profiling on your account. Tell us which domain you'd like to profile.
Behind the scenes, we'll add a per-user Xdebug configuration that:
We'll reply with the trigger value (a short string) and tell you exactly where your cachegrind files will be saved on your account. This configuration step requires server-level access, which is why we set it up for you rather than leaving it as a cPanel task.
Once we've confirmed the configuration is in place, you'll need to enable the Xdebug extension itself in cPanel:
xdebug.
For a full walkthrough of PHP Selector, see our existing article: How to Manage the PHP Version in cPanel Using the Select PHP Version Tool.
This is important: PHP Selector is also how you'll disable Xdebug once you're finished. Even in trigger-only mode, Xdebug being loaded carries a small per-request overhead. Not dramatic - a few hours of testing is fine - but it shouldn't be left enabled indefinitely on a production site.
Profiling is configured to only run when you explicitly ask for it, by adding the trigger value to the request - either as a URL parameter or as a header.
The single most important thing to understand at this step is that you must profile an uncached request. If the page is being served from LiteSpeed Cache, Varnish, a CDN, or any other full-page cache, what gets profiled won't be your slow PHP - it'll be a near-instant cache hit, which tells you nothing.
There are a few ways to get an uncached request:
LSCWP_CTRL=NOCACHE to the URL to force a cache bypass on that request. (Other caches have their own equivalents; check your plugin documentation.)For WordPress sites with LiteSpeed Cache, a typical trigger URL looks like this:
https://www.yourdomain.com/some/page/?LSCWP_CTRL=NOCACHE&cb=123456&XDEBUG_TRIGGER=YOUR_TRIGGER_VALUE
The three parameters serve different purposes:
LSCWP_CTRL=NOCACHE forces LiteSpeed Cache to bypass the cache for this request.cb=123456 is a cache-busting parameter (any random value works). This prevents intermediate caches from serving a stale version.XDEBUG_TRIGGER=YOUR_TRIGGER_VALUE tells Xdebug to profile this request, using the trigger value we sent you.For Magento (or any other site), the same idea applies - append XDEBUG_TRIGGER=YOUR_TRIGGER_VALUE to the URL, along with whatever cache-busting your environment needs:
https://www.yourdomain.com/some/page?cb=123456&XDEBUG_TRIGGER=YOUR_TRIGGER_VALUE
Load the page once. The profile is now captured.
For background processes - AJAX endpoints, webhooks, or cron-driven URLs - you can trigger profiling from the command line using curl. Speak to us if you need help with that.
After triggering the profile, a new file will appear in the directory we told you about, with a name like cachegrind.out.NNNNN.NNNNN. The two numbers are the PHP process ID and a Unix timestamp - useful when you've captured multiple profiles and need to tell them apart.
Download it using either:
If no file appears, get in touch - we can check the Xdebug log on the server, which will usually explain why.
You have three options here, in roughly increasing order of "how much you'd like us to do for you."
QCachegrind (macOS, Linux, Windows) and KCachegrind (Linux) are free utilities for browsing cachegrind files visually. They're the traditional tool for this job and they work very well, though they're not the prettiest software you'll ever use - the interface is functional rather than friendly, and there's a small learning curve. Think of them as a database explorer for performance data: extremely capable, not designed to win design awards.
Installation:
brew install qcachegrind (requires Homebrew, the standard macOS package manager - if you don't have it, the link explains how to install it).sudo apt install kcachegrind on Debian/Ubuntu, or your distribution's equivalent.Open the cachegrind file. The workflow once it's loaded:
{main} or require_once. Work down the list looking for the first function that isn't a top-level entry point - that's usually where the interesting story starts.
The screenshot above shows a real profile loaded in QCachegrind. The Flat Profile (lower left) is sorted by Inclusive time. At the top you can see {main} at 98.32% - the top-level entry point, as expected. Working down past the other top-level entries (get_header, the_content, wp), the first genuinely interesting hot spot is get_shortcode_regex at 11.76% - that's where the investigation begins. Here, apply_filters <cycle 3> has been selected, and the Callers panel on the right shows every function that called it (et_builder_render_layout, the_content, get_body_class, and so on), with call counts. Clicking through those callers traces the chain back to the responsible plugin or theme.

You can see it directly in the profile above: get_shortcode_regex selected on the left, and the All Callers panel on the right showing the breadth of plugins calling into it - WooCommerce Subscriptions, Wordfence, WP_Locale, and others. The Callees panel at the bottom right shows the cost: php::array_map called 14,207 times, accounting for 7.22 of the total 10.5 seconds on its own.
Cachegrind files are large and not designed for human eyes. Modern AI assistants - Claude in particular - are surprisingly good at interpreting them. Claude will write its own analysis code, count function calls, work out the caller chain, and produce a clear narrative explanation of what your site is doing wrong. For non-trivial profiles this can be faster and more readable than working through QCachegrind by hand.
The practical considerations:
File size. Cachegrind files can be sizeable - anywhere from a few hundred KB to tens of MB depending on how complex the request was. Claude's web interface accepts files up to 30 MB per upload, and other AI tools have their own limits. If your file is too large, compress it first: Claude accepts .zip and .tar.gz archives and will decompress them on the other side. A gzip cachegrind.out.NNNNN.NNNNN will typically reduce the file by 80-90%, easily bringing it within the upload limit.
A suggested prompt:
Analyse this Xdebug cachegrind profile. Identify the top functions by inclusive time. Trace the caller chain to identify which plugins or themes are responsible. Summarise the likely root cause of slowness and recommend next debugging steps.
Privacy. Cachegrind files contain function names, file paths, line numbers and timings. They do not contain code, argument values, request bodies, or database query content - so the privacy footprint is smaller than people often assume. What they do contain is file paths that include your cPanel username and the names of every plugin and theme on the site. Have a quick look at the file in a text editor before uploading if any of that is sensitive.
Before uploading to any AI provider, make sure you're on a plan that doesn't use your inputs for model training, or that you've explicitly opted out of training in your account's privacy settings - and confirm that with the provider's current data policy. Different tools have different defaults, and those defaults can change.
If you'd rather not work through this yourself, we offer cachegrind analysis as a paid service. Open a support ticket telling us you'd like to take this option, and we'll send you a quote. Once you confirm, tell us where the file is on your account (or send us the trigger workflow) and we'll produce a written analysis covering: what the bottleneck is, which plugin or theme is responsible, what the underlying mechanism is, and what we'd recommend doing about it. This is usually faster and more decisive than self-analysis, particularly for Magento profiles, which tend to be denser than WordPress ones.
In our experience, slow PHP requests on real-world sites usually trace back to one of a small set of patterns:
has_shortcode(), do_shortcode(), or get_shortcode_regex(), particularly if the site has accumulated lots of registered shortcode tags from various plugins.apply_filters() dominating a profile with hundreds of thousands of calls typically points to a plugin-heavy stack where filters are being called inside tight loops. WooCommerce sites with many extensions are especially prone to this.get_option() or equivalent showing call counts in the tens of thousands usually means something is querying settings inside a loop instead of caching the result once.To put some flesh on the bones: we recently worked with a customer running a Divi-themed WooCommerce site that was taking around 10.5 seconds per page load. Profiling immediately showed where the time was going - 7.3 of those 10.5 seconds were being spent inside the WordPress has_shortcode() function.
The cause was a single function in Divi's WooCommerce integration that was running shortcode checks on every page load, including pages with no WooCommerce content. The site had accumulated 253 registered shortcode tags from various plugins over the years, and has_shortcode() was being called repeatedly against this enormous list. Worse, 108 of those 253 shortcode tags turned out to be completely unused - legacy social-sharing shortcodes from circa 2012, defunct WooCommerce extensions, abandoned page-builder modules. Dead code being checked thousands of times per page.
The thing is, no amount of "deactivate a plugin and refresh" would have found this. The trigger was a check for WooCommerce functionality. The cost was scattered across 253 registered shortcodes. The two were never near each other in any plugin's settings page. Profiling found it in one request.
Identifying the responsible plugin, theme, or module is the hard part. What happens next depends on what you found:
Whichever path you take, the next step is to re-profile the same page and compare. If the function that was at the top of the flat profile has vanished or shrunk, you've confirmed the fix.
Once you've finished investigating:
xdebug extension.The server-side configuration we put in place is harmless once Xdebug itself is disabled, but if you'd like us to remove it entirely, send us a quick ticket and we'll tidy up.
The shift from "the site is slow" to "this specific function consumes 70% of every page load, and here's the line of code that triggers it" is the entire game. Most performance problems aren't really "the whole site is heavy" - they're one or two specific bottlenecks, hiding inside a perfectly capable hosting environment, and a single profile will usually find them in under an hour of careful work. The alternative is weeks of plugin-juggling, intuition, and hope.
If you've got a slow site and you've already done the obvious things - caching, image optimisation, deactivating the plugins you weren't really using anyway - and you're stuck, this is the next step. Open a ticket and we'll get you set up.
Powered by WHMCompleteSolution