Skip to content

Documentation

Performance Architecture

2027-ready allocation-floor database layer with schema-aware conversion, ring-buffer pooling, fused query paths, and zero-tax SDK dispatching.

6/22/2026
15 min read Edit on GitHub

πŸ“Š Performance Benchmarks (2026-2027)

SveltyCMS delivers enterprise-grade performance through its database-agnostic architecture. For the complete set of verified benchmarks, stress test results, and hardware context, please refer to the Performance Benchmarks document.

SQLite β€” Local/Edge (Verified June 2026)

Operation Latency RPS
FIND ONE 0.090 ms 10,386
FIND MANY (50) 0.109 ms 8,863
INSERT 0.142 ms 2,657
DELETE 0.051 ms 14,447
NATIVE UPSERT 0.093 ms 9,102
Peak Throughput β€” 15,617 req/s
LocalCMS SDK Overhead β€” 0.00% (verified parity)

Cross-Database Impact (All 4 Engines)

Optimization SQLite PostgreSQL MariaDB MongoDB
Schema-aware row conversion βœ… Full βœ… Full 🟑 Planned N/A (own path)
Ring-buffer result pool βœ… Full βœ… Full 🟑 Planned βœ… Full
Conditions array pool βœ… Full βœ… Full 🟑 Planned N/A (own path)
Fused mapQuery (no IR objects) βœ… Full βœ… Full 🟑 Planned βœ… Full
for…in (zero Object.entries) βœ… Full βœ… Full 🟑 Planned βœ… Full
Pre-allocated meta object βœ… Full βœ… Full 🟑 Planned βœ… Full
MariaDB double-parse isolated N/A βœ… Benefit 🟑 Planned N/A
Parallel list + count (websiteTokens.getAll) βœ… Full βœ… Full 🟑 Planned βœ… Full

SQL family gets ~5-8 fewer allocations per filtered query. MongoDB gets ~2-3 fewer (result pool + fused mapQuery). All 4 databases benefit from shared BaseAdapter optimizations.

Credential list hot-path

system.websiteTokens.getAll() issues the row query and COUNT(*) in parallel on all four engines (Promise.all in relational-system.ts and website-token-methods.ts). Combined with the { tenantId, name } compound index, admin token tables stay sub-millisecond even under multi-tenant filtering.

Philosophy

SveltyCMS implements best-practice performance patterns from day one. Every feature is built with performance as a core requirement, not an afterthought. The 2027 allocation-floor work pushes the database layer to the practical limit of the current architecture (Drizzle ORM + DatabaseResult contract).

2027 Allocation-Floor Optimizations

Schema-Aware Row Conversion

Instead of checking every row key against DATE_FIELDS/JSON_FIELDS Set lookups on every row read/write, table schemas are pre-computed once during table creation. Only known date and JSON columns are converted β€” eliminating per-row Set.has() calls and enabling targeted iteration.

// Before: checks every key against Sets
for (const k in row) {
  if (DATE_FIELDS.has(k)) {
    /* convert date */
  } else if (JSON_FIELDS.has(k)) {
    /* parse JSON */
  }
  result[k] = row[k];
}

// After: only touches known columns
registerTableSchema("posts", ["_id", "title", "createdAt", "metadata"]);
// converts only "createdAt" (date) and "metadata" (JSON) β€” skips everything else

Fused Query Construction (SQL + MongoDB)

Both mapQuery implementations now build database conditions directly from user queries without intermediate IR (Intermediate Representation) objects. The old pattern (query β†’ QueryIR β†’ LogicalGroup β†’ QueryCondition[] β†’ SQL conditions) is replaced with a single fused walk.

  • SQL path (drizzle-sql-helpers.ts): addFilterConds + addSingleCondition build SQL[] directly
  • Mongo path (mongodb/adapter-core.ts): addMongoConds + addMongoCondition build filter objects directly
  • Eliminated: QueryIR, LogicalGroup, QueryCondition[] intermediates (~3-5 objects per filtered query)
  • Eliminated: All Object.entries() array allocations (replaced with for...in)

Ring-Buffer Result Pool

All BaseAdapter.wrap() calls now use a 64-slot ring-buffer pool for { success, data } result wrappers. Pooled slots are safe in single-threaded JS β€” callers consume results synchronously after await. Eliminates 100% of result wrapper allocations.

Ring-Buffer Conditions Array Pool

Filtered queries (mapQuery) reuse conditions: SQL[] arrays from a 32-slot ring-buffer pool instead of allocating [] each time. Arrays are cleared via .length = 0 without deallocation.

Pre-Allocated Meta Object

The { executionTime } meta sub-object is pre-allocated once per adapter. wrap() mutates this._meta.executionTime instead of creating a new object per call.

MariaDB Double-Parse Isolation

MariaDB’s native JSON columns can double-encode. The double-parse loop is now opt-in (mariaDoubleParseJson: true) β€” PostgreSQL and SQLite paths never pay this cost.

SQLite RETURNING Cache

The insertMany path caches whether RETURNING is supported per SQLite instance. Avoids repeated try/catch + logger.warn overhead in benchmarks and unsupported table scenarios.

Zero-Tax SDK Dispatcher

Self-overwriting getters eliminate Proxy overhead after first access. Verified: 0.00% middleware tax (LocalCMS is statistically identical to direct adapter calls).

Structural Improvements (Shared by All Adapters)

  • Proxy extraction: 3 scattered Proxy factories β†’ 1 shared core/proxy-utils.ts with 23 dedicated tests
  • Tenant filter centralization: ~4+ duplicate sites β†’ single source of truth in relational-utils.ts
  • Query IR removal: standalone query-ir.ts removed; translator inlined into the only 2 real consumers
  • Mongo file reduction: 43 β†’ 39 files (4 thin module/model pairs merged)
  • Code reduction: 1,105 lines deleted across 36 files
  • Lazy Zero-Allocation GraphQL Loaders: Eager per-request user, media, and collection loaders are refactored into on-demand lazy getters, eliminating memory overhead on requests that do not query those fields.
  • Direct Primary Key Index Loading: Replaced MongoDB-style $in filter translations with explicit, database-agnostic db.crud.findByIds index lookups, allowing SQL and document database engines to resolve relational widget fields with zero query-parsing overhead.

Phase 1: Database Indexing & Optimization

Status: βœ… FULLY IMPLEMENTED Impact: Exponential query speedup (O(n) β†’ O(log n))

Background IndexOptimizer service automatically manages database performance through schema-driven indexing, background building, and multi-database support (MongoDB Compound/Text, SQL B-Tree).

Batch Relational Upserts

setMany and bulkUpdate use true multi-row INSERT ... ON CONFLICT DO UPDATE via Drizzle ORM. During setup seeding, ~80 individual DB queries β†’ single batch operation. Dialect-specific handling (PostgreSQL/SQLite excluded.*, MariaDB/MySQL VALUES()).

Negative Caching

O(1) missing-key cache prevents β€œDB miss storms” (verified 2392x speedup for repeated misses).

Prefix-Bucketed Invalidation

O(1) namespace clearing replaces O(N) cache scans.


Database Tuning Recommendations

SveltyCMS ships with zero-configuration defaults that work out of the box with any Docker Compose setup. For production or high-throughput workloads, the following database-level tuning tweaks can yield 2-10x latency improvements without changing any CMS code.

Tip

All tuning below is applied at the database server level (e.g., docker compose command overrides or my.cnf). SveltyCMS adapters adapt automatically β€” no CMS config changes needed.

MariaDB / MySQL

Setting Default (Docker) Recommended Why
max_connections 151 500–1000 Default ceiling easily exhausted under concurrent CMS workloads. Verified: Max_used_connections=152 exceeded the 151 cap, causing connection saturation and 1041ms HTTP latency spikes.
innodb_buffer_pool_size 128 MB 70-80% of available RAM Tiny default forces constant disk reads. For a 4 GB container: innodb_buffer_pool_size=3G. This is the single most impactful tuning knob for MariaDB performance.
innodb_log_file_size 96 MB 512 MB – 2 GB Larger redo logs reduce write contention and checkpoint pressure.
innodb_io_capacity 200 2000–5000 Modern SSDs can handle 50k+ IOPS. Default throttles InnoDB background I/O.
innodb_flush_log_at_trx_commit 1 (fsync per commit) 2 Setting to 2 avoids per-commit fsync (flushes once per second). Caution: trade 1-second durability window for faster writes β€” acceptable for most CMS workloads.
table_open_cache 2000 4000–8000 Reduces table open/close churn when many collections are active.

Measured gains with all tweaks (Intel i7-13700H, Docker, MariaDB 12.2.2):

Operation Default (avg / p95) Tuned (avg / p95) Improvement
INSERT 1.229ms / 3.061ms 0.791ms / 1.100ms -36% / -64%
DELETE 2.044ms / 2.664ms 0.665ms / 0.827ms -67% / -69%
BULK INSERT (100) 5.497ms / 10.170ms 4.460ms / 6.772ms -19% / -33%
TX Commit 4.268ms / 8.675ms 2.250ms / 3.869ms -47% / -55%
TX Rollback 3.016ms / 5.393ms 3.450ms / 6.188ms ~same
INSERT RPS 716 req/s 994 req/s +39%
DELETE RPS 428 req/s 1,293 req/s +202%
TX Commit RPS 225 tx/s 371 tx/s +65%

Read operations (SELECT, FIND) are minimally affected by tuning on small datasets because the default 128MB buffer pool is sufficient for light workloads. Gains compound significantly on production datasets (10k+ entries) and under concurrent multi-tenant load.

Docker Compose override example (docker-compose.override.yml):

services:
  mariadb:
    command:
      - --max_connections=500
      - --innodb_buffer_pool_size=4G
      - --innodb_log_file_size=512M
      - --innodb_io_capacity=2000
      - --innodb_flush_log_at_trx_commit=2

PostgreSQL

Setting Default (Docker) Recommended Why
max_connections 100 200–400 Default caps connection pool quickly. Beyond 400, add PgBouncer for connection pooling.
shared_buffers 128 MB 25% of available RAM Default is too small for active working sets.
effective_cache_size 4 GB 75% of available RAM Helps query planner choose index scans over seq scans.
work_mem 4 MB 16–64 MB Per-sort/join memory. Important for complex relational queries.
wal_buffers 4 MB 64 MB Reduces WAL write contention during bulk operations.
random_page_cost 4.0 1.1 Modern SSDs have near-zero seek cost. Lower value encourages index scans.
synchronous_commit on off Disables per-transaction fsync. Caution: 1-second durability window for ~10x faster writes.

Measured gains with all tweaks (Intel i7-13700H, Docker, PostgreSQL 18.3):

Operation Default (avg / p95) Tuned (avg / p95) Improvement
INSERT 2.201ms / 4.301ms 1.463ms / 1.850ms -34% / -57%
UPDATE 3.036ms / 4.437ms 1.424ms / 1.729ms -53% / -61%
DELETE 2.587ms / 3.112ms 1.200ms / 1.422ms -54% / -54%
NATIVE UPSERT 2.638ms / 3.208ms 1.235ms / 1.454ms -53% / -55%
TX Commit 4.620ms / 7.688ms 4.029ms / 6.843ms -13% / -11%
INSERT RPS 431 req/s 577 req/s +34%
UPDATE RPS 305 req/s 603 req/s +98%
DELETE RPS 333 req/s 703 req/s +111%
HTTP E2E RPS 810 req/s 999 req/s +23%

Write-heavy operations (INSERT, UPDATE, DELETE, UPSERT) benefit most from synchronous_commit=off and larger shared_buffers. Read operations see moderate gains from better query planning (random_page_cost=1.1, effective_cache_size=6GB). The HTTP E2E throughput improves by 23% as the cumulative middleware + DB pipeline becomes more efficient under load.

MongoDB

Setting Default (Docker) Recommended Why
wiredTigerCacheSizeGB 50% of RAM βˆ’ 1 GB 60-70% of available RAM On machines with <8GB RAM, the default may be too small. On machines with >16GB RAM, defaults are already generous and tuning yields minimal gain.
maxConnections 1000 2000+ Default is generous; only raise if you see connection saturation. Connections available on a typical system exceed 400k.
syncdelay 60s Keep default Lower values do NOT improve performance β€” more frequent journal flushes add I/O overhead. Verified: syncdelay=10 made 1000-doc inserts 26% slower (43ms vs 34ms) on the same hardware. Keep default at 60s; reduce only if you need tighter durability guarantees.

Measured baseline (Intel i7-13700H, 31GB RAM, Docker, WiredTiger cache 16GB):

Metric Default Notes
INSERT 1000 docs (1KB each) 34ms (29,412 ops/s) Cache already 16GB β€” no disk bottleneck
READ 1000 docs 14ms (71,429 ops/s) All in cache
HTTP E2E (Truth Audit) 1.175ms avg / 2.157ms p95 851 req/s
Note

On this machine (31GB RAM), MongoDB’s wiredTigerCacheSizeGB default is already ~16GB β€” far larger than the benchmark dataset. The tuning benefit of raising it further is minimal unless you have a dataset exceeding the cache size. On constrained machines (<4GB RAM), explicitly setting wiredTigerCacheSizeGB=1-2GB prevents OOM issues.

MongoDB tuning requires a custom config file mounted into the container (not just command flags). See MongoDB docs for details.

All 4 Adapters Now Run Under Bun

All four database adapters (MariaDB, PostgreSQL, SQLite, MongoDB) run natively under bun test β€” no need for vitest/Node.js for any database.

The fix: A v8 shim in src/utils/v8-shim.ts patches process.getBuiltinModule('v8') to return a safe stub for startupSnapshot.isBuildingSnapshot() β€” the only Bun-incompatible API call in the dependency chain:

Layer Technology Bun compat
ORM Mongoose 9.x βœ… Works
Driver mongodb npm package βœ… Works
Serialization bson βœ… Shimmed β€” v8.isBuildingSnapshot() β†’ returns false

Previously (pre-shim):

NotImplementedError: node:v8 isBuildingSnapshot is not yet implemented in Bun.
    at node_modules/bson/lib/bson.cjs:2610:30

Now: The bson package loads cleanly under Bun, and all MongoDB benchmarks pass (database-performance, transaction-acid, truth-latency, etc.) with no runtime differences from SQL adapters.

The benchmark matrix runner no longer needs special vitest dispatch for MongoDB β€” every database uses bun test uniformly.

SQLite

SQLite is embedded (no Docker tuning needed). For WAL mode (enabled by default in SveltyCMS):

Setting Default Recommended Why
PRAGMA cache_size -2000 (2 MB) -64000 (64 MB) Larger page cache reduces disk reads.
PRAGMA mmap_size 0 (disabled) 26843545600 (26 GB) Memory-maps the database file for zero-copy reads.

These SQLite PRAGMAs are set automatically by SveltyCMS on connection β€” no manual action required.

Redis

Redis is used for caching layers (L2 cache, rate limiting, session store). In-memory β€” tuning focuses on eviction policy, persistence, and connection handling.

Setting Default (Docker) Recommended Why
maxmemory 0 (no limit) 70-80% of available RAM Prevents Redis from consuming all host memory under cache-heavy workloads.
maxmemory-policy noeviction allkeys-lru Evicts least-recently-used keys when memory limit is hit. Prevents write errors on cache saturation.
tcp-keepalive 300s 60s Detects dead connections 5x faster, freeing connection slots under concurrency.
timeout 0 (no timeout) 300s Closes idle client connections after 5 minutes. Prevents connection leak accumulation.
maxclients 10000 20000 Default is generous; only raise if you see connection saturation.
save 3600 1 300 100 60 10000 (RDB snapshots) "" (disable) Disabling RDB persistence avoids fork pauses. Caution: trade durability for latency stability β€” acceptable when Redis is a cache (not primary store).
appendonly no no Keep AOF off for cache-only Redis. Enabling adds fsync overhead with no benefit for ephemeral cache data.

Measured gains with all tweaks (Intel i7-13700H, Docker, redis-benchmark -c 10 -n 5000 -d 100):

Metric Default Tuned Improvement
SET throughput 138,889 req/s 156,250 req/s +12.5%
GET throughput 161,290 req/s 166,667 req/s +3.3%
p50 latency 0.039ms 0.039ms ~same
Important

The raw throughput gain is modest because redis-benchmark uses short-lived connections. The real value of tuning is operational:

  • allkeys-lru prevents OOM write errors when cache fills (infinite improvement vs crashing)
  • tcp-keepalive=60 detects dead connections 5x faster, preventing socket exhaustion under concurrent load
  • timeout=300 auto-reclaims idle connections that would otherwise accumulate over days of uptime
  • Disabling RDB (save "") avoids fork pauses that can spike p99 latency by 100ms+

Docker Compose override example (docker-compose.override.yml):

services:
  redis:
    command:
      - --maxmemory 2gb
      - --maxmemory-policy allkeys-lru
      - --tcp-keepalive 60
      - --timeout 300

Docker Compose Override β€” All Databases

Combine all overrides into a single docker-compose.override.yml alongside your existing compose.yaml:

services:
  mariadb:
    command:
      - --max_connections=500
      - --innodb_buffer_pool_size=4G
      - --innodb_log_file_size=512M
      - --innodb_io_capacity=2000
      - --innodb_flush_log_at_trx_commit=2

  postgres:
    command:
      - -c max_connections=400
      - -c shared_buffers=2GB
      - -c effective_cache_size=6GB
      - -c work_mem=32MB
      - -c wal_buffers=64MB
      - -c random_page_cost=1.1
      - -c synchronous_commit=off

  redis:
    command:
      - --maxmemory 2gb
      - --maxmemory-policy allkeys-lru
      - --tcp-keepalive 60
      - --timeout 300
Note

MongoDB tuning requires a custom config file mounted into the container (not just command flags). See the MongoDB section above for details and the official docs for syntax.


performanceoptimizationcachingindexingdatabasearchitecture
Was this page helpful?