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.
| 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) |
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.
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.
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:
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:
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.
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.
Keep reading
Where to go from here
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