Skip to content

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.

SQLite
361req/s peak

87.8 ms p95 under the mixed load · <0.1% errors

3.45.1

MariaDB
228req/s peak

118 ms p95 under the mixed load · 0% errors

11.4.13-MariaDB-ubu2404

PostgreSQL
189req/s peak

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.

5,000
products
25,000
customers
250,000
orders

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.

5,000
products
25,632
customers
250,678
orders
515,701
order lines
Every table the benchmark seeder wrote, and how many rows it holds
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.

Profile standard · 2026-09-29 · KahunaCart 1.3.6 (cbf5a116) · Grav 2.2.3 · 2.56 ms of every figure below is the network round trip

What answered these requests
  SQLiteMariaDBPostgreSQL
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.

Storefront latency in milliseconds, Published. Each cell is the 95th percentile, with the median and the 99th percentile beneath it.
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.

The admin latency in milliseconds, Published. Each cell is the 95th percentile, with the median and the 99th percentile beneath it.
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.

MCP latency in milliseconds, Published. Each cell is the 95th percentile, with the median and the 99th percentile beneath it.
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 SQLiteMariaDBPostgreSQL
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.

Peak sustainable throughput per route on SQLite: the highest concurrency rung cleared, the requests per second it sustained there, and the rung that broke it.
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.

Peak sustainable throughput per route on MariaDB: the highest concurrency rung cleared, the requests per second it sustained there, and the rung that broke it.
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.

Peak sustainable throughput per route on PostgreSQL: the highest concurrency rung cleared, the requests per second it sustained there, and the rung that broke it.
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 timings in milliseconds by database engine. Each cell is the 95th percentile with the median beneath it.
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.

Every benchmark run recorded, newest first. Latencies are the 95th percentile in milliseconds on the storefront script.
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.

  1. scripts/provision-ubuntu.sh Sets up a bare Ubuntu 24.04 server: nginx, PHP 8.4, MariaDB, PostgreSQL and the tuning described above.
  2. bin/bench --target=remote setup sqlite Builds the site and its database. Repeat per engine.
  3. bin/bench --target=remote seed sqlite Seeds the store and builds its catalog and search indexes.
  4. bin/bench --target=remote run sqlite The ten-minute load test. The capacity command runs the concurrency ladder after it; about 35 minutes per engine for both.
  5. bin/bench report --target=remote Writes the comparison document and the history table.
  6. bin/bench-publish --targets=remote Turns that document into the file this page renders.

Each result records the plugin version, Git commit and database version so it can be traced to the tested build.

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