How much does Native Observability slow Drupal down, and how do you measure it on your server
Deep dive into the Native Observability project
How much does Native Observability slow Drupal down?
Native Observability (opens in a new tab) is the Drupal module that records what happens inside every request, and instrumenting a site with it costs about 4.7 and about 7.0 milliseconds per request on the two machines where I measured it. Same code, same version, same measurement command, two numbers 48% apart.
What the module does and how it is built belongs to the project page. This page is only about what it costs and how you measure that.
The first figure is published in the module documentation (opens in a new tab), which states its conditions and where it came from. I took the second one myself, on a containerised DDEV environment, on 25 September 2026.
| Value | Where | Stated conditions |
|---|---|---|
| ~4.7 ms/req | development laptop, database in Docker | twelve submodules enabled, empty tables, 15 pairs of 6 requests; ~4.6 to ~5.7 across eight runs over two days |
| ~7.0 ms/req | containerised DDEV on an iMac | Drupal 11.4.5, PHP 8.3, Xdebug off, 15 pairs, tables not empty |
| ~1.8 ms/req | same development laptop | without the Execution and Spans submodules. Its own paired run, 9 pairs of 6 requests, against a control of ~4.7 ms/req measured in the same session with eleven submodules |
The third row carries the most weight: switching off two submodules takes the cost from about 4.7 to about 1.8 milliseconds, cutting it by 62%. Both figures behind that cut come from the same paired run, not from comparing the first and third rows of this table. None of the three numbers answers your question, because none of those installations is yours.
Why two machines give two different numbers
What it costs to instrument an application depends on the machine running the extra instruction, and the two machines above differ in everything that matters: CPU clock and generation, opcache state, storage type, competing load at the moment of the measurement, and whatever virtualisation layer sits underneath.
Here is what I did not hold constant: the two Drupal installations did not carry the same content. The documentation states that the lower figure came from empty tables. Mine did not. Data volume moves the work some instrumentation does, so the gap between the two figures reads as a difference between two whole environments, not between two processors. Saying so costs one line and makes the number usable. A figure without its conditions is the same kind of data that APM vendors publish without saying where they got it.
Why the module stopped declaring a percentage
Before 2.0.0, which means up to and including 1.x, native_observability published an estimate nobody had ever measured. The 2.0.0 release notes, which removed that code, put it plainly:
The report used to sum four hardcoded constants and print a made-up "Estimated overhead: 10%".
Four constants written by hand in the source, added up, printed as a percentage. That code went away in 2.0.0. The Drop Times (opens in a new tab) wrote an article about the decision and quotes my written response, where I called that 10% an overestimate drawn from my own testing, one that could not be carried from one server to another. The wording is mine, not theirs: a quotation, not independent confirmation. Their coverage stops at what the module costs, which is one of the things the module does. This page stays on that same ground. The rest lives on the project page.
In its place, 2.0.0 shipped a command. That choice costs something to anyone who looks for a figure on the project page and finds none, and it pays back for anyone deciding whether to install the module on one particular server.
I maintain the module, so this section is my account of my own decision. The measurements you can repeat with the same command. The judgement about the decision stays mine.
The measurements on this page were taken on 2.0.1. The stable release published on the project is 2.0.1, and if that is newer it is worth running the measurement again before trusting the numbers below.
How you measure it: the command
One command, no configuration:
drush no:overhead:measureThe command compares two states of the same installation: capture on and capture off. Measuring both states on one site separates what the module costs from the differences between one server and another, which is exactly what makes comparing your number with mine pointless.
The module ships two calibration routes, kept out of an ordinary measurement: one that runs no query and no rendering, one that runs a declared, constant load. They exist so the measurement asks the same question on any installation.
How the command avoids lying to you
The protocol is the one used for benchmarking in noisy environments, applied to an HTTP request. The 2.0.0 release notes describe it in full, and it comes down to five precautions:
- Paired blocks. Each pair runs one block with capture on and one with capture off, so a server
hiccup hits both sides.
- Alternation. Which side goes first changes from one pair to the next, because the first of the
two pays for the cold start.
- First pair discarded. It warms up caches and opcache and stays out of the calculation.
- Median of the paired differences. A median survives one outlier. A mean does not.
- Exact two-sided sign test, at
p ≤ 0.05, plus a separate pass with capture off on both sides
to measure the machine's noise floor.
That last point is what separates a measurement from a number. Without the noise floor you cannot tell whether the delta you got is the module or your server breathing.
How I read the result
Four values decide whether the number is worth anything, and they all sit in the output. Here is mine, cut down to the lines that matter:
Fixed cost per request (a): 6.953 ms/req
calibration-minimal:
- budget: 0.87% of the declared TTFB budget (good)
- N 15 | IQR [6.908, 7.020] | noise 0.040 | signs 7/7 | p=0.01562| Value | Mine | How to read it |
|---|---|---|
N |
15 | pairs left after the warm-up is discarded. Below 10 the sign test cannot reach significance |
IQR |
[6.908, 7.020] | middle half of the differences. If it is as wide as the value, the measurement is not stable |
noise |
0.040 ms | the machine's noise floor. My effect sits a hundred and seventy-three times above it |
p |
0.01562 | the chance that the observed signs come from randomness. Above 0.05 the tool marks the result unclear |
A delta that does not clear the noise is reported as unclear rather than meas, and the useful move at that point is to measure again on a quieter machine.
Are about 7 milliseconds a lot or a little?
It depends on your site's response budget, and against mine they are about 0.9%.
The reference threshold comes from Solving Big Data Challenges for Enterprise Application Performance Management (opens in a new tab) (Rabl, Gómez-Villamor, Sadoghi, Muntés-Mulero, Jacobsen, Mankovskii, VLDB 2012), which puts it this way: "As a rule of thumb, a maximum tolerable overhead is five percent, but a smaller rate is preferable". Past that threshold, monitoring degrades the thing it is supposed to watch.
Whether the threshold is respected is a separate question. groundcover notes that 3-5% is what turns up in the benchmarks APM vendors run on themselves, and cites a measurement by Scout APM, a New Relic competitor, where the New Relic agent added more than 44% in a Ruby scenario.
| Site TTFB budget | ~7.0 ms are worth | Verdict against the 5% threshold |
|---|---|---|
| 800 ms | ~0.9% | comfortably inside |
| 300 ms | ~2.3% | inside |
| 140 ms | ~5% | at the limit |
I did not pick the 800 milliseconds: that is the value of overhead.budget_ms in the module's configuration, and the comment in the source traces it to the "good" TTFB threshold documented by web.dev. It exists to colour the verdict, and it should be replaced with your site's real budget. A figure in milliseconds, with no budget to compare it against, says nothing about whether the module is sustainable.
The visitor does not pay for the database write
Rows are not written during the request. DeferredPersistenceBuffer collects them in memory and flushes them with a single multi-row INSERT hooked to KernelEvents::TERMINATE, the event Drupal fires after the response has reached the client. The comment in the source says where this came from: a busy request used to ship "around 130 individual INSERTs".
Three details of the mechanism, because the difference between buffering and deferred writing lives there:
- the buffer flushes itself past 1000 rows, so one unusual request does not hold an unbounded volume
in memory;
flush()swallows exceptions, because a fault in the observability must never bring down the
response it was watching;
- the
deferred_persistence_enabledsetting puts the module back to immediate writes, for when you
need to see what the buffer is doing.
What the visitor does pay for is the instrumentation, and that stays inside the request. In the request-level census published in the module documentation (opens in a new tab) and picked up by The Drop Times (opens in a new tab), out of about 6.3 milliseconds of module work about 0.4 sat in the deferred flush (6.1%) while the remaining 93.9% ran before the response left: execution tracking, span linking, cache observation.
That census answers a different question from the fixed cost per request, and the two numbers do not belong in the same column. The census counts the work the module attributes to itself. The paired measurement counts the difference between an instrumented site and the same site without instrumentation, and that is the one the visitor pays.
How much the module collects is up to you, and the limits ship configured
Every data source has a ceiling written into the default configuration, and none of these values is hardcoded. How to set them is covered on the configuration page of the documentation (opens in a new tab). These are the defaults, from config/install.
In native_observability.settings:
| Setting | Default | What it caps |
|---|---|---|
retention.max_rows |
50,000 | total rows kept |
trace_retention_hours |
72 | how long traces live |
spans_retention_hours |
24 | how long execution spans live |
metrics_retention_days |
7 | how long aggregated metrics live |
cleanup_batch_limit |
5,000 | rows deleted per cleanup pass |
The two observers are submodules and carry a configuration object of their own. In native_observability_database_observer.settings:
| Setting | Default | What it caps |
|---|---|---|
slow_query_threshold_ms |
100 | how many milliseconds a query has to take before it is recorded |
max_stored_queries_per_request |
20 | queries kept per request |
retention_hours |
72 | how long recorded queries live |
In native_observability_cache_observer.settings:
| Setting | Default | What it caps |
|---|---|---|
max_stored_invalidations_per_request |
50 | cache invalidations kept per request |
retention_hours |
72 | how long recorded cache events live |
The two per-request ceilings are what keeps the cost flat when a page misbehaves: a route that runs three hundred queries gets twenty of them recorded, not three hundred.
On the personal data side the defaults start closed, and that also lowers the work: the request body, the query string and the headers are not collected (body_mode, query_mode and headers_mode are off), the IP address is anonymised, and only the user agent family is kept. The module also keeps its own routes out of capture (exclude_own_routes), so opening the dashboard does not inflate the counters you are reading.
What the other tools declare
None of the PHP APM vendors publishes a figure obtained on the customer's machine, and two of the four I checked publish no figure at all. The column that matters is not what they declare, it is under which conditions.
| Tool | What it publishes | Under which conditions |
|---|---|---|
| Tideways | 4.93% on PHP 5.6 and 17.11% on PHP 7 for Timeline at 10% sampling, 13.41% and 23.86% for XHProf | the oss-performance benchmark against WordPress, with the machine pushed to its limit and every core saturated. The vendor warns that "for actual applications the overhead is smaller" and that the result "is just representive of WordPress" |
| Blackfire | "close-to-no overhead" on standard traces, and up to 15% measured on Extended Traces | maximum observed by the vendor, with Drupal named explicitly alongside Symfony, Prestashop 1.7+ and Ibexa DXP |
| New Relic | no figure in the documentation I checked (opens in a new tab), which explains how to reduce overhead | not applicable |
| Datadog | no figure in the documentation I checked (opens in a new tab), which covers agent rate limits | not applicable |
The Tideways percentages do not compare with mine. They measure WordPress on PHP 5.6 and PHP 7, under a load built to saturate the machine, while my figure comes from Drupal 11.4.5 on PHP 8.3. A different CMS, two major PHP versions apart, opposite conditions.
What does compare is the behaviour. Tideways and Blackfire publish measured numbers and state their limits, which is the right thing to do even when the number that comes out is unflattering. New Relic and Datadog, on the pages I read, publish nothing. That is the comparison that holds, and it has no percentages in it.
What to ask whoever publishes a performance number
Four questions, and they hold for any tool, not only this one. They are not theoretical: I wrote them after finding three defects that skewed my own module's measurement, each listed with its number in the 2.0.0 release notes (opens in a new tab).
- Does the tool exclude itself from its own measurement? Anything that also traces the routes
it measures with is counting itself, and the direction of the error is not predictable: in my case the cost came out 55% lower, not higher.
- Is the repeat run protected from the cache? If the second run can be served from the first
run's cache, the published number is the cache's timing, roughly two orders of magnitude lower, so the cost on the page is one that nobody pays.
- Are the settings reread? A service that reads its settings once, for the lifetime of its own
instance, works under PHP-FPM and lies in a persistent runtime. That is the class of problem behind the core meta issue on persistent application servers (opens in a new tab), which lists ReactPHP, PHP-PM, PHPFastCGI, FrankenPHP and Swoole.
- Is the noise floor stated? Without it nothing separates the measured effect from the
machine's own breathing, which is why noise sits next to the value in the command's result.
If whoever publishes a number cannot answer these four, that number is worth what the "10%" printed by 1.x was worth.
What I do with this number
I measure before installing in production, and I measure again after any change of machine or of PHP version. The procedure is four steps:
- Install the module on an environment matching production in PHP, Drupal and database version,
with Xdebug off: Xdebug instruments every function call, and any measurement taken with it running belongs in the bin.
- Run
drush no:overhead:measurewith no other load on the machine. - Read
N,IQR,noiseandpbefore you read the value. If the verdict isunclear, the value
does not get used.
- Divide the cost by the site's response budget and compare it with the threshold you set yourself.
The installation steps are in the module documentation (opens in a new tab), and what the module actually does is on the project page on this site. The number you get holds for your machine and that installation. I published mine so it can serve as a reference point for the order of magnitude, and so it is visible that two measurements of the same code can sit half a millisecond or two milliseconds apart.
Sources and references
All sources were checked on 27 September 2026.
- Native Observability, project page (opens in a new tab). Requirements, measurement command, stable release 2.0.1.
- Native Observability documentation: what the module costs (opens in a new tab). The figures 4.684, 1.795, 6.253 and 0.379 with their conditions and the provenance table.
- Native Observability documentation: configuration (opens in a new tab) and installation (opens in a new tab).
- Native Observability 2.0.0, release notes (opens in a new tab). Removal of the constant-based estimate, paired measurement protocol, sign test.
- Native Observability 2.0.0 Measures Its Own Performance Cost, The Drop Times (opens in a new tab). Request-level census, 6.253 ms and the breakdown of the deferred flush.
- APM Overheads: Is Your APM Slowing You Down?, groundcover (opens in a new tab). The 3-5% in vendor benchmarks, and the independent measurement above 44% in a Ruby scenario.
- Profiling Overhead and PHP 7, Tideways (opens in a new tab). Vendor-measured overhead on PHP 5.6 and PHP 7, from 4.93% to 23.86%.
- Configuring Blackfire Monitoring (opens in a new tab). "close-to-no overhead" on standard traces, up to 15% measured on Extended Traces.
- PHP agent overhead reduction tips, New Relic (opens in a new tab). No declared figure.
- Agent Rate Limits, Datadog (opens in a new tab). No declared figure. The page covers agent rate limits.
- Robust benchmarking in noisy environments (opens in a new tab). Operating system noise, warm-up, outlier filtering.
- Make Drupal compatible with persistent app servers like ReactPHP, PHP-PM, PHPFastCGI, FrankenPHP, Swoole, core #2218651 (opens in a new tab). The class of defect behind settings read once per service instance.
- Solving Big Data Challenges for Enterprise Application Performance Management, Rabl et al., VLDB 2012 (opens in a new tab). The 5% threshold as a rule of thumb.
This content was produced by AI and edited by a person.
How was AI used?
Drafts are produced with AI assistance and then directed, edited and fact checked by a person. Figures, dates and version numbers are verified against their public sources before publication, and the date of that check is stated in the text.