Skip to content

Performance

Performance tested at 250,000 orders

We tested identical datasets on SQLite, MariaDB and PostgreSQL: 5,000 products, 25,000 customers and 250,000 orders on a dedicated 8 vCPU server, with the load generated from a separate machine in the same data centre. Results are measured at the load generator and published with the workload, hardware and code revision.

Server runs of 2026-09-29 on KahunaCart 1.3.6 (commit cbf5a116) and Grav 2.2.3. Every earlier run, including 1.3.5 on the same server and the August runs on the previous 2 vCPU server, is in the history table.

Why it is fast

How the store reduces request work

The implementation combines caching, indexing, aggregate reports and background jobs. Each addresses a different part of the workload.

  • No database in the request path for content

    Grav stores content in files and serves it through the page cache, reducing database work for content pages.

  • A database for commerce

    Commerce records use indexed tables and transactions. The benchmark loads the same seeded data into each supported engine.

  • A denormalised catalog index and a facet cache

    Catalog listings read a product index, pick the page of product ids first and load only those products. Attribute counts are cached until the catalog changes and the category counts in the navigation are kept between requests, so a category page does not recount them each time.

  • Daily report rollups

    Daily summaries let reports read aggregated data instead of processing the full order history for every request.

  • Indexed order search, one query per grid

    Order search tries an exact order number or email before a wider match over indexed fields. On PostgreSQL, catalog search ranks from a stored search column. The product search index is rebuilt in the background, one rebuild at a time, and searches use the database until the new index is ready. Request-level caching and batched product data reduce repeated queries.

  • Email and webhooks are queued jobs

    The worker handles email and outbound webhooks after checkout, keeping external delivery latency out of the checkout request.

Route by route

Response times by route and database

The table shows client-measured 95th-percentile latency in milliseconds, including a network round trip of 1 to 3 ms. The lowest result in each row is highlighted.

95th percentile latency in milliseconds per route, on Published. The lowest figure in each row is highlighted.
Route SQLite p95 ms MariaDB p95 ms PostgreSQL p95 ms
Storefront Cart, checkout and receipt requests are not served from the page cache.
Home page 52.2 (fastest) 84.2 79.7
Catalog, first page 64.1 (fastest) 89.8 104
Catalog, deep page with filters 136 (fastest) 319 229
Category listing 80.3 (fastest) 105 124
Product page 52.7 (fastest) 79.1 99.7
Search results 82.3 (fastest) 118 128
Add to cart 61.8 (fastest) 82.1 107
Cart 58.9 (fastest) 79.5 98.5
Checkout form 70.8 (fastest) 95.8 118
Checkout submit 78.5 (fastest) 125 163
Signed receipt 48.4 47.7 (fastest) 72.2
The admin, under the same load Admin requests were measured while the storefront load test was running.
Order list 106 143 101 (fastest)
Summary report 180 160 132 (fastest)

Published run, SQLite 2026-09-29, MariaDB 2026-09-29, PostgreSQL 2026-09-29. KahunaCart commit cbf5a116, Grav 2.2.3, PHP 8.4.26. Median and 99th percentile for every cell are on the benchmarks page.

SQLite was the fastest of the three on every storefront route except the order receipt, where MariaDB was ahead by less than a millisecond. PostgreSQL was fastest on the report summary, which reads all 250,000 orders, and just ahead of SQLite on the order list. MariaDB's order search took 604 ms in the 1.3.5 run; 1.3.6 tries an exact order number or email first, and the same search now takes 49 ms, the fastest of the three. MariaDB and PostgreSQL connect over their Unix sockets. In the 1.3.5 runs over TCP, PostgreSQL was about 30% slower on storefront routes because Ubuntu's PostgreSQL negotiates TLS on every connection. The socket and TCP runs are both in the history table.

Before and after

Before and after the performance changes

In August, before KahunaCart 1.0, two builds were tested on the previous 2 vCPU server using the same dataset and scripts. Catalog p95 fell from 7,394 to 204 ms on SQLite and from 20,904 to 332 ms on MariaDB. MariaDB's error rate fell from 1.21% to zero.

The changes included the catalog index, cached facet counts, indexed order search, request-level caching and batched product-grid queries. The server and seeded store were unchanged.

Those runs used a smaller server and put the load generator on the public internet, so compare them with each other rather than with the current figures above.

Catalog page, 95th percentile latency After the pass, commit 4866f88, August 2026
SQLite 204 ms
MariaDB 332 ms
PostgreSQL 512 ms
0200400600
The build before, commit dfc883d, on a scale 35 times longer
SQLite 7,394 msnow 204
MariaDB 20,904 msnow 332
PostgreSQL 2,093 msnow 512
07,00014,00021,000

Runs of 2026-08-30 on the previous 2 vCPU server, against the same 5,000 product, 250,000 order store. The thin line on each lower bar marks the build after the pass on the same axis. Both rows for each engine are in the history table.

Capacity

Capacity under increasing concurrency

The test increases concurrent virtual users through 12, 24, 36, 48, 72, 96, 144 and 192, holding each level for 20 seconds. The server has 24 PHP-FPM workers. A level passes when p95 stays below 500 ms with no errors; the chart shows throughput at the highest passing level.

SQLite Cleared means p95 under 500 ms, zero errors, 20 s per rung, 24 FPM workers
Home 351.6 req/s144 visitors, p95 441 ms
Catalog page 1 274.2 req/s96 visitors, p95 383 ms
Catalog filtered 289.8 req/s96 visitors, p95 365 ms
Category 251.8 req/s96 visitors, p95 425 ms
Product 361.1 req/s144 visitors, p95 431 ms
Search 257.6 req/s96 visitors, p95 419 ms
0100200300400
MariaDB Cleared means p95 under 500 ms, zero errors, 20 s per rung, 24 FPM workers
Home 208.8 req/s72 visitors, p95 384 ms
Catalog page 1 191.3 req/s72 visitors, p95 421 ms
Catalog filtered 193.9 req/s72 visitors, p95 414 ms
Category 188.9 req/s72 visitors, p95 429 ms
Product 227.6 req/s96 visitors, p95 459 ms
Search 185.6 req/s72 visitors, p95 438 ms
0100200300400
PostgreSQL Cleared means p95 under 500 ms, zero errors, 20 s per rung, 24 FPM workers
Home 189.4 req/s72 visitors, p95 463 ms
Catalog page 1 142.7 req/s48 visitors, p95 488 ms
Catalog filtered 138.9 req/s48 visitors, p95 460 ms
Category 134.7 req/s36 visitors, p95 399 ms
Product 154.4 req/s48 visitors, p95 418 ms
Search 139.6 req/s48 visitors, p95 476 ms
0100200300400

The server has 8 vCPUs and 15.6 GB of memory. Virtual users make requests without pauses, so their count is not equivalent to the same number of real shoppers. All three engines were tested, search included. On the same server, KahunaCart 1.3.6 with Grav 2.2.3 took the SQLite home page from 224 to 352 requests per second, MariaDB category pages from 69 to 189 and MariaDB search from 75 to 186. Search now holds up well on every engine.

Methodology

What these measurements exclude

These tests measure server responses. The following exclusions matter when interpreting them for a live store.

Browser rendering and media. The test does not run JavaScript, process CSS or fetch images. It does not measure visual load time.

Email delivery. Messages are queued. The checkout measurement excludes delivery by the worker.

Payment-provider latency. Checkout uses the test provider without an external payment request. Live payment flows add provider and network latency.

A CDN. Nothing in the harness honours the storefront's cache headers. Every request reaches Grav.

Maximum mixed-workload capacity. The standard run has a fixed arrival rate. The capacity test increases load on individual routes separately.

Run it yourself

The harness records the plugin version, Git commit, database version and connection method for each result, and it measures a committed revision rather than a working tree. On a bare Ubuntu 24.04 server, the published run comes down to these steps, repeated for each engine:

TXT
scripts/provision-ubuntu.sh          # once, as root on the server
scripts/build-remote-source.sh       # once, the Grav install the bench site is copied from
bin/bench --target=remote setup sqlite
bin/bench --target=remote seed sqlite
bin/bench --target=remote run sqlite
bin/bench --target=remote capacity sqlite
bin/bench report --target=remote
bin/bench-publish --targets=remote

The two commands the harness leans on ship inside the plugin, so you can seed a store this size and time its queries on your own server without the harness at all:

TXT
bin/plugin kahunacart bench:seed
bin/plugin kahunacart bench:queries

The harness lives at github.com/kahunacart/kahunacart-bench. The benchmarking guide covers what each query stands for and how to seed a store this size safely.

Operations

Check that report summaries are up to date

Reports can use daily summary tables to reduce query work. The Health screen shows how current those summaries are. After an import or restore, bin/plugin kahunacart reports:rebuild rebuilds them. Reports identify whether they used summaries or queried order records.

  • Health checks cover the database, migrations, scheduler, mail, downloads, proxies, providers, webhooks, templates and report summaries.
  • Scheduled recalculation includes the last three closed days so recent changes are reflected.
  • An outdated summary triggers a dashboard alert.
yoursite.com/admin/kahunacart#/health
The KahunaCart Health screen inside the Grav admin, listing installation checks with a status for each

Questions

Common questions

Which database should I use?

SQLite is the default and needs no separate database server. MySQL, MariaDB and PostgreSQL are alternatives when they better fit your hosting or operations. In our test SQLite was the fastest of the three on every storefront route but the order receipt, and PostgreSQL was fastest on the report screens. Results vary by workload; compare the route and query measurements rather than choosing from a single figure.

How does order volume affect performance?

Indexed order queries and daily report summaries reduce the work needed as order history grows. The published dataset includes 250,000 orders, 512,000 order lines and 1.87 million history rows. Your own workload and hosting still determine response times.

Are image loads measured?

No. The harness fetches server responses without loading page images. Image size, caching and delivery need separate testing for browser performance.

Why retain slower runs?

Keeping earlier results makes changes and regressions visible. The history includes the 5.87% error run on SQLite and the pre-change MariaDB catalog p95 of 20,904 ms. The same seed makes comparisons between builds more useful. The only run taken out is an early KahunaCart 1.3.6 run whose search figures were measured while a since-fixed bug was rebuilding the search index under load.

Review the results or run your own test.

The tables are generated from the public benchmark harness. Use the same dataset and scripts to test your hosting.

See the full data