Performance
One dataset, three database engines
The benchmark uses 5,000 products, 25,000 customers and 250,000 orders generated from a fixed seed. The tables compare response times and query performance on SQLite, MariaDB and PostgreSQL, with earlier runs retained for comparison.
Storefront under load on a dedicated 8 vCPU server, profile standard, 2026-09-29, KahunaCart 1.3.6 with Grav 2.2.3.
87.8 ms p95 under the mixed load · <0.1% errors
3.45.1
118 ms p95 under the mixed load · 0% errors
11.4.13-MariaDB-ubu2404
127 ms p95 under the mixed load · 0% errors
16.15 (Ubuntu 16.15-0ubuntu0.24.04.1)
The mixed workload includes storefront browsing, carts and test checkout, with concurrent admin API requests.
The store
The benchmark dataset
Each engine receives identical records generated from the same seed and timestamp. This keeps the data consistent when comparing engines or builds.
The store the query timings ran against
The in-process bench:queries figures further down were taken on this store, with 25 timed iterations per query after 5 untimed ones.
| Table | Rows |
|---|---|
| kahunacart addresses | 31,271 |
| kahunacart adjustments | 703,015 |
| kahunacart attributes | 6 |
| kahunacart categories | 51 |
| kahunacart coupon scopes | 64 |
| kahunacart coupon usages | 27,670 |
| kahunacart coupons | 50 |
| kahunacart customer auth | 9,945 |
| kahunacart customers | 25,000 |
| kahunacart download grants | 43,894 |
| kahunacart order history | 1,874,490 |
| kahunacart order items | 511,731 |
| kahunacart orders | 250,000 |
| kahunacart product attributes | 16,098 |
| kahunacart product categories | 7,742 |
| kahunacart product files | 725 |
| kahunacart product media | 12,498 |
| kahunacart product option values | 10,774 |
| kahunacart product options | 3,638 |
| kahunacart product releases | 878 |
| kahunacart product tags | 14,881 |
| kahunacart products | 5,000 |
| kahunacart refund items | 19,180 |
| kahunacart sale rules | 108 |
| kahunacart sale tiers | 3 |
| kahunacart sales | 6 |
| kahunacart shipment items | 381,465 |
| kahunacart shipments | 210,925 |
| kahunacart shipping methods | 12 |
| kahunacart shipping zones | 4 |
| kahunacart tags | 40 |
| kahunacart tax rates | 16 |
| kahunacart tax zones | 8 |
| kahunacart transactions | 262,473 |
| kahunacart variant option values | 19,766 |
| kahunacart variants | 15,239 |
Every row derives from seed 20260830 at a fixed clock of 1788048000. Nothing in the seeder calls a random or a clock function, so the three engines hold the same rows byte for byte and a difference in a column is a difference in the engine.
The generated orders include matching items, adjustments, shipments and status history. The seeder uses KahunaCart's connection layer to create a consistent commerce dataset.
The results
Response times by route and engine
Latency is measured at the client in milliseconds. The main figure is the 95th percentile: 95% of measured requests completed within that time. Median and 99th-percentile figures show more of the distribution.
Published Published
A dedicated server, with the load generator on a separate machine in the same data centre. The network round trip (about a millisecond) is inside every figure. These are the numbers to quote.
What answered these requests
| SQLite | MariaDB | PostgreSQL | |
|---|---|---|---|
| Server | AMD EPYC 7742 64-Core Processor · 8 cores · 15.6 GB | AMD EPYC 7742 64-Core Processor · 8 cores · 15.6 GB | AMD EPYC 7742 64-Core Processor · 8 cores · 15.6 GB |
| Operating system | Ubuntu 24.04.5 LTS (6.8.0-90-generic, x86_64) | Ubuntu 24.04.5 LTS (6.8.0-90-generic, x86_64) | Ubuntu 24.04.5 LTS (6.8.0-90-generic, x86_64) |
| PHP | 8.4.26 | 8.4.26 | 8.4.26 |
| Engine version | 3.45.1 | 11.4.13-MariaDB-ubu2404 | 16.15 (Ubuntu 16.15-0ubuntu0.24.04.1) |
| Load generator | k6 v2.3.0 on a separate 2 vCPU VPS in the same data centre, over the private network | k6 v2.3.0 on a separate 2 vCPU VPS in the same data centre, over the private network | k6 v2.3.0 on a separate 2 vCPU VPS in the same data centre, over the private network |
| Path to the server | direct to origin | direct to origin | direct to origin |
| Run id | 20260929T223558Z | 20260929T220207Z | 20260929T212751Z |
Storefront
What a shopper waits for.
| Route | SQLite p95 ms | MariaDB p95 ms | PostgreSQL p95 ms |
|---|---|---|---|
| Home page | 52.2 43.4 / 58.3 | 84.2 63.6 / 98.0 | 79.7 58.7 / 91.9 |
| Catalog, first page | 64.1 51.5 / 71.6 | 89.8 67.4 / 106 | 104 72.9 / 118 |
| Catalog, deep page with filters | 136 69.3 / 154 | 319 90.0 / 358 | 229 99.4 / 271 |
| Category listing | 80.3 57.1 / 91.6 | 105 68.7 / 146 | 124 79.0 / 151 |
| Product page | 52.7 41.4 / 58.1 | 79.1 57.8 / 91.6 | 99.7 69.2 / 115 |
| Search results | 82.3 58.3 / 96.2 | 118 72.6 / 164 | 128 81.6 / 153 |
| Add to cart | 61.8 37.7 / 85.1 | 82.1 58.1 / 94.5 | 107 73.1 / 124 |
| Cart | 58.9 44.7 / 67.2 | 79.5 57.2 / 89.3 | 98.5 67.1 / 113 |
| Checkout form | 70.8 53.4 / 81.9 | 95.8 68.8 / 109 | 118 81.3 / 130 |
| Checkout submit | 78.5 45.1 / 133 | 125 78.3 / 140 | 163 104 / 189 |
| Signed receipt | 48.4 33.8 / 89.9 | 47.7 34.2 / 62.4 | 72.2 52.1 / 79.1 |
The admin
Taken while the storefront load was running, not on an idle site.
| Route | SQLite p95 ms | MariaDB p95 ms | PostgreSQL p95 ms |
|---|---|---|---|
| Order list | 106 93.4 / 123 | 143 116 / 157 | 101 72.1 / 115 |
| Order search | 89.2 28.4 / 96.5 | 49.5 30.5 / 56.2 | 99.2 46.2 / 107 |
| One order | 31.5 25.1 / 44.5 | 39.7 28.8 / 52.6 | 61.4 46.5 / 70.3 |
| Customer search | 91.7 81.3 / 98.5 | 132 105 / 139 | 145 111 / 153 |
| Summary report | 180 158 / 206 | 160 131 / 178 | 132 112 / 136 |
| Product report | 402 351 / 429 | 515 455 / 541 | 340 278 / 359 |
MCP
The tool manifest and three representative tool routes.
| Route | SQLite p95 ms | MariaDB p95 ms | PostgreSQL p95 ms |
|---|---|---|---|
| Tool manifest | 71.5 62.4 / 75.2 | 67.9 59.1 / 70.9 | 75.9 63.4 / 82.7 |
| get_config | 31.7 27.1 / 34.4 | 32.4 27.2 / 35.2 | 44.4 36.7 / 49.5 |
| list_orders | 101 90.7 / 105 | 129 113 / 147 | 86.4 71.0 / 98.0 |
| get_order | 29.6 24.6 / 31.1 | 32.3 26.9 / 33.1 | 55.5 46.8 / 61.5 |
Whole-run totals
Every request the script made, not one route. The storefront and the admin scripts ran at the same time; the MCP pass ran on its own.
| Script | Figure | SQLite | MariaDB | PostgreSQL |
|---|---|---|---|---|
| Storefront | Requests per second | 34.45 | 33.26 | 32.88 |
| 95th percentile (ms) | 87.8 | 118 | 127 | |
| Requests | 20,780 | 20,072 | 19,830 | |
| Errors | <0.1% | 0% | 0% | |
| Admin API | Requests per second | 5.00 | 5.00 | 5.00 |
| 95th percentile (ms) | 351 | 453 | 276 | |
| Requests | 3,001 | 3,000 | 3,001 | |
| Errors | 0% | 0% | 0% | |
| MCP | Requests per second | 37.78 | 36.60 | 36.86 |
| 95th percentile (ms) | 95.3 | 120 | 79.8 | |
| Requests | 1,164 | 1,127 | 1,136 | |
| Errors | 0% | 0% | 0% |
A tinted cell is the lowest 95th percentile in its row. An em dash is a route that engine's run did not exercise, not a route that failed. Generated 2026-09-29T23:08:09Z from the harness results.
Capacity
Capacity as concurrent requests increase
Each route is tested separately at increasing concurrency. The result is the highest level that kept p95 below 500 ms with no errors. This supplements the fixed-rate mixed workload above.
SQLite
Rungs of 12, 24, 36, 48, 72, 96, 144, 192 concurrent visitors, 20 seconds each, against a PHP-FPM pool of 24 workers. A rung is cleared when the 95th percentile stays under 500 ms with no errors.
Peak sustained: 361 req/s, product page.
| Route | Cleared at | Req/s | p95 ms | Broke at |
|---|---|---|---|---|
| Home page | 144 VUs | 352 | 441 | 192 VUs |
| Catalog, first page | 96 VUs | 274 | 383 | 144 VUs |
| Catalog, deep page with filters | 96 VUs | 290 | 365 | 144 VUs |
| Category listing | 96 VUs | 252 | 425 | 144 VUs |
| Product page | 144 VUs | 361 | 431 | 192 VUs |
| Search results | 96 VUs | 258 | 419 | 144 VUs |
MariaDB
Rungs of 12, 24, 36, 48, 72, 96, 144, 192 concurrent visitors, 20 seconds each, against a PHP-FPM pool of 24 workers. A rung is cleared when the 95th percentile stays under 500 ms with no errors.
Peak sustained: 228 req/s, product page.
| Route | Cleared at | Req/s | p95 ms | Broke at |
|---|---|---|---|---|
| Home page | 72 VUs | 209 | 384 | 96 VUs |
| Catalog, first page | 72 VUs | 191 | 421 | 96 VUs |
| Catalog, deep page with filters | 72 VUs | 194 | 414 | 96 VUs |
| Category listing | 72 VUs | 189 | 429 | 96 VUs |
| Product page | 96 VUs | 228 | 459 | 144 VUs |
| Search results | 72 VUs | 186 | 438 | 96 VUs |
PostgreSQL
Rungs of 12, 24, 36, 48, 72, 96, 144, 192 concurrent visitors, 20 seconds each, against a PHP-FPM pool of 24 workers. A rung is cleared when the 95th percentile stays under 500 ms with no errors.
Peak sustained: 189 req/s, home page.
| Route | Cleared at | Req/s | p95 ms | Broke at |
|---|---|---|---|---|
| Home page | 72 VUs | 189 | 463 | 96 VUs |
| Catalog, first page | 48 VUs | 143 | 488 | 72 VUs |
| Catalog, deep page with filters | 48 VUs | 139 | 460 | 72 VUs |
| Category listing | 36 VUs | 135 | 399 | 48 VUs |
| Product page | 48 VUs | 154 | 418 | 72 VUs |
| Search results | 48 VUs | 140 | 476 | 72 VUs |
One VU is one relentless visitor with no think time, so a rung of 20 VUs stands for far more than twenty real shoppers. "Broke at" is the first rung where the route missed the ceiling; the quoted figures are from the last rung it cleared.
All three engines were tested on the same seeded store and 8 vCPU server. On SQLite, the home page reached 352 requests per second and product pages 361 before p95 passed 500 ms. Compared with KahunaCart 1.3.5 on this same server, KahunaCart 1.3.6 with Grav 2.2.3 took category pages from 115 to 252 requests per second on SQLite, from 69 to 189 on MariaDB and from 74 to 135 on PostgreSQL. Search is now strong on every engine: 258 requests per second on SQLite, 186 on MariaDB and 140 on PostgreSQL, up from 125, 75 and 73.
Under the pages
Database query timings
bin/plugin kahunacart bench:queries times the repository and service methods used by the store. These in-process measurements exclude HTTP, Twig rendering and network latency.
| Query | Rows | SQLite p95 ms | MariaDB p95 ms | PostgreSQL p95 ms |
|---|---|---|---|---|
| catalog listing, page 1 | 24 | 3.93 3.47 median | 14.3 9.94 median | 5.79 4.82 median |
| catalog listing, deep page | 3 | 4.77 4.67 median | 13.8 12.8 median | 7.63 7.28 median |
| product page by slug | 6 | 0.30 0.27 median | 2.20 2.11 median | 2.60 2.35 median |
| catalog search | 24 | 10.6 10.4 median | 27.2 24.8 median | 9.24 8.25 median |
| catalog listing, three facets | 17 | 5.96 5.77 median | 8.92 7.52 median | 9.25 8.85 median |
| catalog listing, on sale | 24 | 4.41 4.24 median | 4.68 4.21 median | 2.54 2.34 median |
| catalog facet counts | 309 | 6.35 6.14 median | 17.7 16.9 median | 15.6 11.0 median |
| catalog search, two words | 4 | 0.53 0.46 median | 2.41 2.29 median | 1.40 1.28 median |
| admin order list, page 1 | 25 | 0.26 0.21 median | 0.78 0.69 median | 1.40 1.30 median |
| admin order list, deep page | 25 | 29.7 26.7 median | 47.7 46.2 median | 30.1 29.3 median |
| order search by email | 15 | 19.9 15.7 median | 5.14 4.88 median | 15.2 14.3 median |
| order search by number | 1 | 0.35 0.32 median | 1.16 1.06 median | 1.22 1.12 median |
| order detail | 8 | 0.21 0.19 median | 1.75 1.61 median | 1.78 1.69 median |
| customer by email | 1 | 0.02 0.01 median | 0.18 0.16 median | 0.26 0.21 median |
| report, revenue by month | 25 | 118 110 median | 347 307 median | 104 96.0 median |
| report, top products | 10 | 113 110 median | 365 310 median | 103 95.0 median |
| cart pricing, 5 lines | 5 | 0.75 0.74 median | 3.68 2.72 median | 4.38 3.13 median |
| stock decrement and restore | 1 | 0.19 0.17 median | 1.93 1.75 median | 2.63 2.26 median |
Taken on a store of 250,678 orders and 5,000 products, 25 timed iterations per query after 5 untimed ones. The benchmark leaves the store as it found it: the cart it prices is removed afterwards and the stock it takes is put back. A row where an engine reads — is one that engine's run did not record.
Order search and report queries are useful comparisons on a large order history. See Benchmarking for the query definitions and test conditions.
Run history
Results across builds
Every run against the same seeded store. The September 2026 rows are from the dedicated 8 vCPU server, with the load generator in the same data centre: KahunaCart 1.3.5 first (over Unix sockets and over TCP), then the KahunaCart 1.3.6 release run with Grav 2.2.3. An earlier 1.3.6 run was withdrawn because a search index bug, now fixed, was rebuilding the index under load while it ran. The August rows are pre-1.0 builds on the previous 2 vCPU server, with the load generator on the public internet, so compare rows from the same server rather than across the two. Regressions stay in the table too.
| Date | Target | Engine | Build | Version | Commit | Profile | Req/s | Catalog p95 | Product p95 | Checkout p95 | Errors |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 2026-09-29 | Server | SQLite | 1.3.6 with Grav 2.2.3, 8 vCPU server | 1.3.6 | cbf5a116 |
standard | 34.45 | 64.06 | 52.74 | 78.52 | 0.01% |
| 2026-09-29 | Server | MariaDB | 1.3.6 with Grav 2.2.3, 8 vCPU server | 1.3.6 | cbf5a116 |
standard | 33.26 | 89.84 | 79.1 | 124.67 | 0.00% |
| 2026-09-29 | Server | PostgreSQL | 1.3.6 with Grav 2.2.3, 8 vCPU server | 1.3.6 | cbf5a116 |
standard | 32.88 | 104.35 | 99.72 | 163.49 | 0.00% |
| 2026-09-29 | Server | PostgreSQL | 1.3.5, 8 vCPU server | 1.3.5 | 95ac2e03 |
standard | 30.98 | 174.66 | 122.27 | 175.68 | 0.00% |
| 2026-09-29 | Server | MariaDB | 1.3.5, 8 vCPU server | 1.3.5 | 95ac2e03 |
standard | 30.6 | 151.03 | 121.58 | 137.36 | 0.00% |
| 2026-09-28 | Server | PostgreSQL | 1.3.5, 8 vCPU server, database over TCP | 1.3.5 | 2712a27c |
standard | 29.65 | 222.35 | 174.47 | 248.52 | 0.00% |
| 2026-09-28 | Server | MariaDB | 1.3.5, 8 vCPU server, database over TCP | 1.3.5 | 2712a27c |
standard | 30.74 | 156.01 | 126.72 | 152.7 | 0.00% |
| 2026-09-28 | Server | SQLite | 1.3.5, 8 vCPU server | 1.3.5 | 95ac2e03 |
standard | 33.33 | 93.86 | 74.18 | 72.55 | 0.00% |
| 2026-08-31 | Laptop | SQLite | Wave 4 + SQLite search fix | 0.1.0 | 5e5e7b8 |
standard | 22.98 | 22.04 | 24.03 | 15.43 | 0.00% |
| 2026-08-31 | Server | SQLite | Wave 4 + SQLite search fix | 0.1.0 | 5e5e7b8 |
standard | 6.48 | 219.82 | 184.67 | 171.25 | 0.00% |
| 2026-08-30 | Server | MariaDB | Wave 4 | 0.1.0 | 62bb24f |
standard | 6.35 | 273.56 | 272.53 | 190.66 | 0.00% |
| 2026-08-30 | Server | SQLite | Wave 4 | 0.1.0 | 62bb24f |
standard | 6.84 | 213.78 | 181.49 | 147.28 | 5.87% |
| 2026-08-30 | Server | PostgreSQL | Wave 4 | 0.1.0 | 62bb24f |
standard | 5.54 | 648.12 | 537.64 | 425.26 | 0.00% |
| 2026-08-30 | Laptop | PostgreSQL | Wave 4 | 0.1.0 | 62bb24f |
standard | 21.8 | 65.33 | 61.9 | 34.89 | 0.00% |
| 2026-08-30 | Laptop | SQLite | Wave 4 | 0.1.0 | 62bb24f |
standard | 23.01 | 24.69 | 24.61 | 12.69 | 6.16% |
| 2026-08-30 | Laptop | MySQL | Wave 4 | 0.1.0 | 62bb24f |
standard | 21.96 | 53.7 | 56.35 | 26.89 | 0.00% |
| 2026-08-30 | Laptop | PostgreSQL | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 22.13 | 58.25 | 58.88 | 34.66 | 0.00% |
| 2026-08-30 | Laptop | SQLite | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 23.1 | 23.42 | 25.03 | 15.76 | 0.00% |
| 2026-08-30 | Laptop | MySQL | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 22.18 | 49.09 | 54.41 | 31.5 | 0.00% |
| 2026-08-30 | Server | MariaDB | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 6.23 | 332.36 | 279.59 | 254.63 | 0.00% |
| 2026-08-30 | Server | SQLite | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 6.76 | 204.0 | 171.0 | 141.02 | 0.00% |
| 2026-08-30 | Server | PostgreSQL | Wave 3 + storefront perf | 0.1.0 | 4866f88 |
standard | 5.64 | 511.79 | 505.36 | 403.52 | 0.00% |
| 2026-08-30 | Server | MariaDB | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 0.79 | 20903.94 | 27110.54 | 7854.19 | 1.21% |
| 2026-08-30 | Server | SQLite | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 1.09 | 7394.24 | 6059.37 | 6942.92 | 0.00% |
| 2026-08-30 | Server | PostgreSQL | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 3.72 | 2092.99 | 1493.53 | 1163.95 | 0.00% |
| 2026-08-30 | Laptop | PostgreSQL | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 21.91 | 76.52 | 60.66 | 176.33 | 0.00% |
| 2026-08-30 | Laptop | SQLite | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 22.27 | 64.61 | 47.49 | 26.25 | 0.00% |
| 2026-08-30 | Laptop | MySQL | pre-Wave-3 | 0.1.0 | dfc883d |
standard | 1.43 | 12984.89 | 12279.85 | 12225.9 | 0.00% |
A Server row includes the network round trip and a Laptop row does not, so compare a row with rows of its own target. 28 runs recorded.
Methodology
Workload, environment and exclusions
Use these conditions to interpret the results and reproduce the tests.
Load-test workload
The storefront script requests the home page, catalog pages, filtered listings, categories, products and search. It also adds items to carts and completes test checkouts. The workload is approximately 85% browsing, 10% cart activity and 5% purchases.
The admin script runs concurrently at a fixed low arrival rate. It tests order lists, order and customer searches, order details and reports. A separate MCP script checks the tool manifest and representative tool routes.
Grav's page cache is enabled. Cart, checkout, receipt and account pages use no-store and are not served from that cache.
Exclusions
The test does not execute browser JavaScript, process CSS or load and resize images. It measures server responses rather than visual page-load time.
Email delivery runs through background jobs and is excluded from checkout latency. Payments use the test provider, so the results also exclude external payment-provider latency.
Every request reaches Grav; the workload does not measure CDN delivery. The fixed-rate mixed test does not establish maximum capacity. The separate capacity test increases concurrency for individual routes.
Server environment
The published server has 8 vCPUs (AMD EPYC 7742) and 15.6 GB of memory, running Ubuntu 24.04, nginx 1.24 and PHP 8.4. It runs nothing but the benchmark, and only the database engine under test is running during a run.
The stack is tuned the way a dedicated store server would be: a static pool of 24 PHP-FPM workers, opcache with timestamp checks off and the JIT on, APCu for Grav's cache, a 6.4 GB InnoDB buffer pool for MariaDB and the usual pgtune settings for PostgreSQL. Durability is left at each engine's defaults, so no figure comes from risking a committed order. MariaDB and PostgreSQL are reached over their Unix sockets, with KahunaCart's optional persistent connections left off. The repository's provisioning script sets all of this up on a bare server.
The load generator is k6 on a separate 2 vCPU server in the same data centre, reaching the store over the private network with a round trip of 1 to 3 ms. The round trip is included in route timings and recorded with the environment details. The server uses MariaDB 11.4 rather than MySQL 8.4; the tables identify the engine actually tested.
Per-IP store and API rate limits are disabled on the benchmark site because the load generator sends all requests from one address. This is a test-only configuration and is not appropriate for a live store.
Local measurements
Laptop runs share hardware between the application and load generator. They are useful for comparing engines and builds on that machine, but their absolute timings should not be treated as server results.
Repeatability
The harness reuses the same seed and timestamp across runs and retains results in the history table. Server runs measure a committed revision of the plugin, so every published figure names a commit that exists.
Reproduce it
The public repository contains the setup, load tests and report generation. The commands below generate the data displayed on this page.
-
scripts/provision-ubuntu.shSets up a bare Ubuntu 24.04 server: nginx, PHP 8.4, MariaDB, PostgreSQL and the tuning described above. -
bin/bench --target=remote setup sqliteBuilds the site and its database. Repeat per engine. -
bin/bench --target=remote seed sqliteSeeds the store and builds its catalog and search indexes. -
bin/bench --target=remote run sqliteThe ten-minute load test. The capacity command runs the concurrency ladder after it; about 35 minutes per engine for both. -
bin/bench report --target=remoteWrites the comparison document and the history table. -
bin/bench-publish --targets=remoteTurns that document into the file this page renders.
The harness repository: github.com/kahunacart/kahunacart-bench
Each result records the plugin version, Git commit and database version so it can be traced to the tested build.
Keep reading
Further reading
Run the benchmark on your hosting.
Use the supplied seed and query commands or the full public harness. Compare the results with the workload your store needs to handle.
Read the benchmarking guide