Skip to content

Documentation

Performance Architecture

2027-ready allocation-floor database layer with findPage/hasMore, count modes (exact|estimate|auto), short-lived count cache, schema-aware conversion, and zero-tax SDK dispatching.

8/9/2026
37 min read Edit on GitHub
On this page

πŸ“Š 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 August 2026)

Operation Latency RPS
FIND ONE 0.008 ms 100–119k
FIND MANY (50) 0.081 ms 10,397–11,507
INSERT 0.038 ms 14,170
UPDATE 0.080 ms 9,341–11,195
DELETE 0.049 ms 14,010–14,422
NATIVE UPSERT 0.067 ms 11,838–12,227
Peak Throughput β€” 390,617 req/s
LocalCMS SDK Overhead β€” 0.00% (verified parity)
Note

Adapter findOne (100–119k RPS) is ~50% of the raw SQLite engine ceiling (~205k) β€” the remaining delta is Drizzle AST + date conversion on the non-raw path. findMany with a pure _id filter routes through the same raw path and measures ~111k RPS. INSERT uses a raw INSERT…RETURNING fast path (85% of the 16.7k ceiling) β€” the benchmark measures the production path (the ambient BENCHMARK env no longer toggles skipReturning; only the explicit seed option does).

Core database layer (all 4 engines)

Optimizations live in shared core, not one dialect:

Shared module Engines Effect
core/lookup-query.ts + SqlAdapterCore.findOne SQLite, PostgreSQL, MariaDB Pure {_id} / {_id,tenantId} β†’ findById (skips full mapQuery)
SqlAdapterCore.findMany id ultra path SQLite, PostgreSQL, MariaDB Pure {_id} list calls β†’ findById (after before-hooks, same semantics as findOne); ~10Γ— faster than dynamic-SQL list path
MongoCrudMethods.findMany id ultra path MongoDB Same shape: {_id} list calls β†’ lean findOne, tenant from options or query
SqlAdapterCore.findById + SQLite rawFindById SQLite raw SELECT; PG/Maria eq+limit Fast primary-key reads
SqlAdapterCore.update re-read via findById All SQL when RETURNING empty / non-returning Faster than post-update findOne
insertReturnsRows / updateReturnsRows SQLite + PostgreSQL (Drizzle RETURNING); MariaDB raw UPDATE…RETURNING fast path Single RT on all four engines
Raw INSERT…RETURNING fast path PostgreSQL, MariaDB, SQLite One-RT insert via tagged template / prepared pool.execute / wrapped prepared SQL β€” skips Drizzle AST on write. PG rawInsertReturning honors skipReturning (no-read-back synthesis, seed/system callers)
Raw UPDATE…RETURNING (PG) + no-read-back skipReturning PostgreSQL, MariaDB, SQLite PG: tagged-template UPDATE (314 β†’ 444 RPS, 99% of ceiling). skipReturning: true reconstructs the row from prepared values (full-doc callers): MariaDB UPDATE ~2Γ— (431 β†’ 828, up to 1.2k), SQLite +52% (10.5k β†’ 16.1k)
Raw multi-VALUES insertMany (all SQL) SQLite, PostgreSQL, MariaDB Single atomic statement per chunk (999-param SQLite / 65k-param PG & MariaDB). SQLite BULK (100) 177 β†’ 248 RPS (+40%); PG 87 β†’ 233 RPS (+168%, was Drizzle .returning()); MariaDB is engine-bound (~4.6ms/100 rows) so parity with Drizzle but now raw + skipReturning for outbox/seeds. Undefined values bind as column defaults: literal DEFAULT (PG/MariaDB) or explicit fills (SQLite β€” no DEFAULT keyword in VALUES; fills createdAt/updatedAt/status/isDeleted/data, fixing a NULL-createdAt bug on the raw path)
Raw single INSERT (MariaDB) MariaDB Drizzle mysql2 has no .returning() and pays the AST per insert β€” raw no-returning pool.execute + values synthesis: INSERT 219–271 β†’ 279–317 RPS (+15%)
skipReturning bulk (SQLite) SQLite + outbox flush + seeds Seed/system-bulk callers get prepared values WITHOUT JSON.parse/flatten (was a no-op win before): bulk profile 401 β†’ 565 batches/s; outbox flush wired to skipReturning
Prepared-statement cache sizing (MariaDB) MariaDB maxPreparedStatements: 2000 (env MARIADB_MAX_PREPARED) mysql2 default (100) overflowed under Drizzle content queries β†’ re-prepare churn
Prepared-statement cache (SQLite) SQLite client.prepare wrapped per connection Drizzle re-preps every query; cache turns per-call sqlite3_prepare (~4Β΅s) into a Map hit
Projection (options.fields) SQLite, PostgreSQL, MariaDB Prunes the JSON data blob from SELECT + skips JSON.parse/flatten when all requested fields are physical
deleteMany single statement SQLite, PostgreSQL, MariaDB One UPDATE/DELETE instead of findMany + N deletes
quoteIdentifier dialect hook SQLite, PostgreSQL, MariaDB Backtick identifiers on MariaDB (double quotes are strings without ANSI_QUOTES)
Composite index (tenantId, status, updatedAt) SQLite createModel DDL + PG/Maria createModel Serves the canonical tenant list query (WHERE tenantId=? AND status=? ORDER BY updatedAt DESC LIMIT n) from one index β€” measured 194 β†’ 68k RPS at 100k rows on SQLite (no temp B-tree); PG uses updatedAt DESC for index-order sort
Mongo composite index {tenantId, status, updatedAt} MongoDB createIndexes Same canonical list query served from one index (previously {tenantId,status} + separate sort β†’ temp sort)
MongoCrudMethods + shared isIdLookupQuery MongoDB Lean findOne fast path; single-tenant bare _id allowed
Mongo insertOne MongoDB Avoids Mongoose Document + full validate graph on insert
Wire compression override (MONGO_COMPRESSORS) MongoDB none disables zstd/snappy (CPU-bound on small payloads β€” measured INSERT 711β†’812, FIND ONE 881β†’1,032 RPS on LAN); default auto-detect for WAN bandwidth savings
Mongo date-walk skip + 3-key mapQuery fast path MongoDB Content schemas store ISO strings β€” processDates walk skipped; safeQuery {_id,tenantId,isDeleted} shape bypasses rebuild
convertDatesToISO zero-work fast path All SQL In-place conversion returns the row untouched when no Date instances and no JSON-string columns (content rows store ISO strings); on the zero-work path an already-parsed blob (PostgreSQL jsonb arrives as an object, not a string) is still flattened into the row so blob fields stay visible; skipJson: true opts out (projection-only reads)
Row-store hybrid (scalar fields β†’ columns) All SQL SELECTIVE materialization: only fields with a query benefit become physical columns β€” indexed/unique fields (real constraints + indexed filters/sorts) or an explicit materialize: true opt-in, always scalar-shaped (allowlist in drizzle-sql-helpers). All other fields stay in the data blob, keeping rows narrow on network adapters (every extra column costs a bind on writes + a decode on reads; measured PG INSERT +51% / FIND MANY +99% when ALL scalars were materialized, while unindexed columns only served filters). Filters/sorts on materialized fields use real columns/indexes; the read merge treats columns as authoritative (data fills only gaps); legacy rows are backfilled from data on column creation (idempotent UPDATE); raw findById selects materialized columns; boolean columns coerce 0/1 β†’ true/false on the raw paths; atomicIncrement targets the column when materialized. Also fixed a pre-existing silent ALTER no-op (PRAGMA ran through stmt.run()), PG/Maria dead columns (createModel made them; getTable never registered them), and the stale-materialized-column drift class (createModel never DROPPED columns/indexes from earlier full-materialization runs β€” dead indexes measurably slowed writes; tables rebuilt under the selective policy recovered the BULK INSERT regression)

| LocalCMS and HTTP turbo sit above this layer β€” same gains on every adapter:

SDK write-path slice Before After Note
Lazy module singletons (workflow/response-cache/pub-sub/outbox) 3.5k create / 3.4k update 4.0k / 6.3k per-write await import() cost 30–60Β΅s each β€” resolved once, hot path = promise resolves
Plugin-registry views (getAll() / hook presence) allocates mapped array + hook walk per mutation O(1) cached (invalidated on register) called twice per SDK create/update by triggerLifecycleHook
Outbox emit (buffered) crypto UUID + getDb() per emit crypto UUID stamped at emit time (~1Β΅s β€” part of the emit contract: callers read event._id); only the DB write is deferred to the 25ms/64-event bulk flush; buffered path is env-check + push measured 28Β΅s β†’ ~5Β΅s
Audit hook (after-insert) logs every insert incl. outbox flush batches skips svelty_outbox (internal machinery); sync-first flags; skipReturning bulk flush the outbox→audit cascade (1000-entry flushes on the write path) was the hidden tax
Full-pipeline SDK create (SQLite) 2.7k RPS 3.7–7.6k RPS outbox delta vs disabled: +121.8% β†’ +22–75%; detached 10–14.4k (host-load dependent; range measured on the isolated benchmark DB)
Note

Benchmark DB isolation (2026-08): local benchmarks used to silently fall back to the live default name (config/test-database/sveltycms.db) because loadPrivateConfig returns null without DB_HOST and the SQLite adapter then guessed a filename. Benchmarks wrote into the same growing file across runs (38k SdkVsDirect rows, 380k benchmark_crud rows), which made every later run measure a bigger table β€” earlier sub-4k SDK-create readings were this pollution, not a regression. Fixes: benchmark harness sets DB_HOST explicitly; the SQLite adapter now fails closed in test/benchmark mode when no DB_NAME is derivable (never silently uses sveltycms.db); config/database/ remains the only live-data folder.

HTTP turbo / LocalCMS β†’ ICrudAdapter.findOne|insert|update
  β†’ SqlAdapterCore (SQLite | Postgres | MariaDB)
  β†’ MongoCrudMethods (MongoDB)

LocalCMS collections hot path (create / update / findById)

Slow SDK entry paths historically paid for work that was not needed on simple schemas. Current design (CollectionsNamespace in src/services/sdk/namespaces/collections-namespace.ts):

Optimization Effect
Schema hot flags (_hasActiveWidgets, _hasNumberFields, _hasSanitizableFields, _hasHooks) Skip field walks, hooks import, and modifyRequest when unused
findById single-id β†’ crud.findOne Avoid $in findMany + full batch machinery cost for one id
findMany pure-_id β†’ findById (after before-hooks) List path with an id filter pays raw-SELECT speed (~10Γ—) instead of dynamic SQL
Same-tick batch window for concurrent findById Still coalesces N+1 without always using multi-id queries
Sync FNV query-hash for list cache keys No async hash-wasm on every find()
Static CacheCategory import No dynamic import on cache set
Entry mutations do not bump contentStore.contentVersion Avoids nav/SSE structure invalidation on every write
Path + alias indexes on contentStore O(1) getNodeByPath / collection alias lookup
Workflow definition negative cache (60s) Create path skips DB miss for collections without workflows
Parallel afterMutation + afterSave Write post-hooks no longer serialize
Outbox kill-switch pre-check before dynamic import Write microtask skips the outbox module import when DISABLE_OUTBOX=true (the only opt-out; ambient benchmark env toggles removed)
Lazy module singletons on the write path Per-write await import(workflow/response-cache/pub-sub/outbox) cost 30–60Β΅s each even when cached; resolve once, hot path becomes a promise resolve β€” full-pipeline create 3.5k β†’ 4.0k RPS, update 3.4k β†’ 6.3k RPS
Deduped cache-invalidation patterns Per-write pattern fan-out cut from 9 clears to 6 (identical generated keys)

SDK tax suite: tests/benchmarks/local-cms-crud.test.ts measures cms.collections.create / update / findById vs adapter findOne (not raw db.crud alone). tests/benchmarks/local-sdk-vs-direct.test.ts reports the full pipeline (all post-write side effects executing: outbox INSERT, cache-pattern invalidation, workflow init, pubsub β€” create β‰ˆ2.0k RPS, update β‰ˆ2.5k RPS on SQLite) and the detached number via the documented skipSideEffects: true option (create β‰ˆ4.9k, update β‰ˆ4.4k). No environment flags are involved β€” benchmarks run the same code path production runs.

HTTP findById turbo lane + responseCache

Authenticated GET short-circuit (handleTurboGet β†’ L1 responseCache):

Fix Why
successResponse stashes apiBody Turbo/write path can re-use one stringify
Weak-ETag path still writes responseCache Collections findById used weak ETag and previously never warmed turbo L1
Sync generateContentEtag Dropped async hash-wasm on GET miss path
Real session turbo warm Benchmark logins populate turboAuthCache via the real auth pipeline β€” turbo fires with real cookies (benchmark pseudo-sessions removed 2026-08)
API cache writes use buildUserResponseCacheKey Same key as handleTurboGet (was writing only cacheService under a different key)
# Rebuild required before HTTP turbo numbers pick up hook/handler changes
bun run build
bun test tests/benchmarks/api-latency.test.ts   # cold (cache-busted) + TURBO-HIT
bun test tests/benchmarks/local-cms-crud.test.ts

When enterprise auth extras are disabled/optional: middleware still enforces core auth + RBAC for protected routes; SAML/SCIM/2FA/passkeys add cost only when configured (see Authentication System).

How to re-baseline after changes:

BENCHMARK_RECORD=1 bun test tests/benchmarks/database-performance.test.ts
bun test tests/benchmarks/local-api-throughput.test.ts
bun test tests/benchmarks/api-latency.test.ts
bun test tests/benchmarks/local-cms-crud.test.ts
bun test tests/benchmarks/mixed-workload.test.ts
bun run test:unit -- tests/unit/services/local-cms.test.ts

Middleware & Request Lane Pipeline (Verified August 2026)

Every request is classified in O(1) by classifyRequest() (src/hooks/request-classifier.ts) before the middleware sequence. Responses carry x-svelty-lane for observability and load-balancer routing.

Request Lane Latency RPS
Turbo Pipeline (Light) 0.294 ms 3,130
Full Security + Auth 0.884 ms 962
REST with API Caching 0.856 ms 1,029
GraphQL Query Caching 0.865 ms 1,018
Health Check Fast Path 0.572 ms 1,749
Static Asset Fast Path 0.586 ms 1,524

What each lane does

Lane When Pipeline cost
FAST_STATIC /favicon.ico, /robots.txt, /_app/*, /static/* Skip full stack; long-cache headers
HEALTH /health, /api/system/health Immediate JSON health (no auth/DB wait)
HYPER_TURBO Authenticated GET/HEAD on cacheable API prefixes + warm session handleTurboGet may serve pre-stringified body + ETag
API_READ Public/unauthenticated GET API, or non-cacheable GET Full security + auth path
API_WRITE POST/PUT/PATCH/DELETE Full CSRF + RBAC + mutation pipeline
APP_SSR Admin pages (/dashboard, /config, /collections, … + locales) Full SSR pipeline
BOOTSTRAP /setup, /login, /auth Setup/auth shell
FILES /files/*, /media/* downloads File server path
PUBLIC_SITE Non-admin public pages Public SSR

Cacheable API prefixes (eligible for HYPER_TURBO when a session cookie or Authorization is present): /api/collections, /api/content, /api/settings, /api/system, /api/graphql, /api/media, /api/dashboard, and related admin namespaces listed in CACHEABLE_PREFIXES.

GraphQL / REST response cache (operator view)

Path Warm hit (typical) What is skipped on hit
HTTP GET (Turbo lane) ~0.25–0.35 ms Full handler body recompute; uses user-scoped response cache
HTTP POST GraphQL (handler) ~0.45–0.65 ms Yoga re-parse / JIT cold cost when L1 has body+etag tuple
LocalCMS in-process ~0.016 ms No HTTP; still zero-tax vs direct adapter

Cache keys are user-scoped (u:{userId}:…) with deep-sorted GraphQL variables so multi-tenant / multi-user isolation holds. Mutations invalidate via responseCache (res:* / GraphQL patterns).

Tests (pyramid):

Layer File Contract
Unit tests/unit/core/request-classifier-and-response-cache.test.ts All lanes + auth_sessions turbo detection + GraphQL key isolation
Unit tests/unit/hooks/handle-turbo-get-lane.test.ts TURBO-HIT vs miss, Host cookie, POST skip
Integration tests/integration/api/request-lane-headers.test.ts Live x-svelty-lane + health JSON + dashboard __data.json
E2E tests/e2e/routes/system/hooks-lane-smoke.spec.ts Admin session shell + /health browser request
Bench tests/benchmarks/lane-router-attribution.test.ts Classification throughput

Cross-Database Impact (All 4 Engines)

Optimization SQLite PostgreSQL MariaDB MongoDB
Schema-aware row conversion βœ… Full βœ… Full βœ… Full N/A (own path)
Ring-buffer result pool βœ… Full βœ… Full βœ… Full βœ… Full
Conditions array pool βœ… Full βœ… Full βœ… Full N/A (own path)
Fused mapQuery (no IR objects) βœ… Full βœ… Full βœ… Full βœ… Full
for…in (zero Object.entries) βœ… Full βœ… Full βœ… Full βœ… Full
Pre-allocated meta object βœ… Full βœ… Full βœ… Full βœ… Full
MariaDB double-parse isolated N/A βœ… Benefit βœ… N/A
Parallel list + count (websiteTokens.getAll) βœ… Full βœ… Full βœ… Full βœ… Full
Prepared-statement cache βœ… N/A (driver caches) N/A (driver prepared) N/A
Projection (fields β†’ skip data blob) βœ… βœ… βœ… βœ… (driver)
Single-statement deleteMany βœ… βœ… βœ… βœ… (native)
Raw UPDATE…RETURNING write fast path βœ… βœ… (Drizzle) βœ… βœ… (findOneAndUpdate)

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() keeps admin tables sub-millisecond under multi-tenant filtering ({ tenantId, name } compound index):

  • MongoDB β€” crud.findPage with limit+1 hasMore + total: "exact" (count goes through the 30s L1 wrapper)
  • SQL engines β€” parallel SELECT … LIMIT n+1 + COUNT(*) (Promise.all in relational-system.ts); sentinel row trimmed before scrubbing hashes

Product-layer list & count (equal on all engines)

Design rule: Prefer cheaper correctness tiers by default. Exact cardinality is opt-in. Optimizations live in the shared ICrudAdapter contract β€” not engine-specific call sites β€” so SQLite, PostgreSQL, MariaDB, and MongoDB gain together.

findPage + hasMore (limit + 1)

Admin and API list UIs no longer need findMany + count for β€œnext page?” semantics.

const page = await db.crud.findPage(
  "posts",
  { status: "publish" },
  {
    limit: 50,
    tenantId,
    total: "none", // default β€” no COUNT(*)
  },
);
// page.data = { items, hasMore, pageSize, nextCursor? }
Option Behavior
total: "none" (default) Single query with limit + 1; hasMore from row count
total: "exact" Adds exact count after the page fetch
total: "estimate" / "auto" Adds approximate total when safe (see count modes)

Implementation: SqlAdapterCore.findPage, MongoCrudMethods.findPage, pure helpers in core/page-utils.ts. Tenant guard forwards findPage. Schema proxy: db.posts.findPage({ filter, limit, cursor }).

Keyset cursor (deep pages)

nextCursor is an opaque base64url payload (id + optional sort field/value + direction). Pass it as options.cursor on the next call:

  • Ignores offset when cursor is set (avoids OFFSET N scans)
  • Filter merge uses Mongo-style $lt/$gt/$or/$and β€” SQL mapQuery and Mongo both understand them
  • Default sort when omitted: { _id: -1 } for stable keyset order
  • Legacy plain _id strings still decode as cursors

When total is requested, findMany and count run in parallel (count still hits the 30s L1 cache).

Unified count(..., { mode })

Mode When Semantic
exact Billing, RBAC, uniqueness True cardinality
estimate Dashboards / badges when unfiltered Engine stats / metadata
auto (default) General Estimate if empty filter + no tenantId; else exact

Estimate is never used for tenant-scoped filters (would leak whole-table cardinality). Eligibility: shouldUseEstimateCount() in core/page-utils.ts.

Engine Estimate path
MongoDB estimatedDocumentCount() (WiredTiger metadata)
PostgreSQL pg_class.reltuples
MariaDB / MySQL information_schema.TABLES.TABLE_ROWS
SQLite sqlite_stat1 when ANALYZE present; else exact (already sub-ms)

Short-lived tenant count cache (L1 / L2)

createCountCachedCrud (core/count-cache.ts) wraps crud.count after the tenant guard in db.ts:

  • TTL: 30 seconds (COUNT_CACHE_TTL_SECONDS)
  • Key: count:{collection}:{mode}:{includeDeleted}:{filterHash} (tenant-namespaced by cacheService)
  • Tags: count, count:{collection}, collection:{collection} β†’ cleared with invalidateQueryCache
  • Bypass: options.bypassCache: true

Equal benefit: after the first miss, repeated admin badge / dashboard counts hit L1 (~0.024 ms on all engines in the 2026-08-04 re-baseline).

Verified gains (2026-08-04 re-bench, local Docker / embedded)

Suite: BENCHMARK_RECORD=1 bun test tests/benchmarks/database-performance.test.ts
Hardware: same host as prior adapter baseline. Redis disabled. Includes keyset scenarios.

Scenario SQLite PostgreSQL MariaDB MongoDB
FIND PAGE (50 hasMore) 0.086 ms 1.008 ms 0.664 ms 0.867 ms
LIST+COUNT (legacy) 0.192 ms 2.233 ms 1.086 ms 4.925 ms
findPage vs legacy ~2.2Γ— ~2.2Γ— ~1.6Γ— ~5.7Γ—
FIND PAGE keyset (page 2) 0.072 ms 0.996 ms 0.645 ms 0.748 ms
FIND MANY offset 50 0.050 ms 0.904 ms 0.585 ms 0.705 ms
COUNT exact (filtered, no cache) 0.120 ms 2.197 ms 0.936 ms 3.817 ms
COUNT ESTIMATE (unfiltered) 0.023 ms 0.745 ms 0.502 ms 0.492 ms
COUNT CACHED (L1 hit) 0.024 ms 0.024 ms 0.029 ms 0.025 ms
estimate vs exact ~5Γ— ~3Γ— ~1.9Γ— ~7.8Γ—
cached vs exact ~5Γ— ~90Γ— ~32Γ— ~150Γ—
Note

Networked engines benefit most from dropping the second round-trip (LIST+COUNT β†’ findPage) and from count cache. Keyset vs shallow OFFSET 50 is near-parity; prefer cursor for deep pages (OFFSET 10k+) where cost grows with N.

getCollectionData now uses findPage with total: "auto" only when includeMetadata is true; list-only paths skip count entirely.

PostgreSQL raw-path identifier quoting fix (2026-08-11)

Bug: the PG adapter’s raw findById and raw UPDATE…RETURNING paths interpolated the table name unquoted (exec.unsafe(getTableName(table))). PostgreSQL folds unquoted identifiers to lowercase, so any mixed-case collection table (e.g. collection_BenchmarkStable) raised 42P01 relation does not exist on every call and silently fell back to the slower Drizzle path β€” the raw fast paths had never engaged for mixed-case collections (the only kind the content system creates). MariaDB/SQLite were unaffected (backticks / case-insensitive identifiers).

Fix: quote the identifier in both raw paths ("${name.replace(/"/g, '""')}").

Measured (honest production server, real session, Docker PostgreSQL, tmp-profile probe):

Cell Before After
HTTP update (client-observed) 6.21 ms 4.34 ms (βˆ’30%)
Update handler + adapter (ns:persist) 3.1–5.9 ms 1.7–2.5 ms
Random read (cache miss) 0.70 ms 0.57 ms

Reads/writes on mixed-case collections now use the prepared raw statements (parse-once + bind/execute reuse) exactly like lowercase test collections did.

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). Product-layer list/count contracts (above) reduce round-trips before that floor matters.

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

Registrations are additive β€” knowledge only grows: a later registerTableSchema call that includes materialized/boolean columns augments an earlier base-only registration (and a later partial registration never shrinks the maps). Both the physical (collection_…) and logical table names are registered so raw and Drizzle paths share the same schema knowledge.

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.


Operator Guide: Further Server Performance Gains

SveltyCMS ships with strong defaults. Use this checklist on top of the lane/cache stack above β€” none of it requires forking core code.

1. Request lanes & application cache (CMS-level)

Goal What to do
Maximize Turbo GET hits Keep real session cookies (auth_sessions / __Host-auth_sessions / __Secure-auth_sessions) or Bearer tokens on cacheable GETs; anonymous traffic stays on API_READ and never uses turbo auth.
Warm GraphQL reads Prefer idempotent GETs with stable query + variables for public/catalog data; POST still caches when the handler warms responseCache, but GET turbo short-circuits earlier.
L2 shared cache across nodes Enable Redis (USE_REDIS / private config) so cacheService L2 + response-cache L2 are shared; without Redis, each node only has L1 Maps.
Avoid self-inflicted misses Content mutations already invalidate response cache; custom bulk scripts should call the same invalidation paths (or restart) so clients do not serve stale turbo bodies.
Observe lanes in production Log or sample x-svelty-lane at the reverse proxy; high API_WRITE or low HYPER_TURBO share usually means missing auth headers on GETs or traffic outside CACHEABLE_PREFIXES.
Health checks Point load balancers at /health (lane HEALTH) β€” not admin SSR β€” so probes stay sub-ms and do not warm auth/session paths.
LocalSDK vs HTTP Server-side code should use LocalCMS / Local SDK (documented zero-tax path). Never fetch('/api/…') from +page.server.ts for hot paths.

2. Database server tuning (engine-level)

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.

⚑ Application Environment Overrides for Low-Latency Development & Benchmarking

SveltyCMS supports native environment variable overrides to tune database session parameters directly from .env or process environment, eliminating the need to modify external database container files:

Environment Variable Target Database Default Fast Dev / Benchmark Setting Impact & Behavior
PG_SYNCHRONOUS_COMMIT PostgreSQL on off or local Bypasses per-transaction disk fsync. Reduces write latencies from ~15ms down to ~1-2ms for local dev and benchmarks.
PG_WORK_MEM PostgreSQL Default (4MB) 32MB or 64MB Prevents disk spilling for complex sorts/joins during dynamic content queries.
SQLITE_SYNCHRONOUS SQLite NORMAL NORMAL / OFF Controls SQLite synchronous write durability (NORMAL recommended for WAL mode).
SQLITE_BUSY_TIMEOUT SQLite 30000 30000 Busy lock wait timeout (30s) preventing concurrent writer lock errors under load.
SQLITE_WAL_AUTOCHECKPOINT SQLite 2000 2000 or 5000 Page count for auto-flushing dirty WAL pages to main file, smoothing out p99 tail spikes.
MARIADB_SESSION_INIT MariaDB None Custom SQL Allows executing session-level SQL commands per new connection checked out from the pool.
TEST_MODE MongoDB w: 1, j: true β€” Sandbox isolation only (bench DB + test config); the adapter never changes write concern from environment flags.
Tip

Developer Recommendation: For maximum developer velocity during local testing, migration seeding, or benchmark runs, set PG_SYNCHRONOUS_COMMIT=off in your .env file or export it in your shell environment.

πŸš€ Core CMS Application Code Optimizations (Universal Across All 4 Adapters)

In addition to database-level session settings, SveltyCMS incorporates pure application-layer code optimizations in the core API dispatcher, schema validation engine, and audit pipeline. These zero-allocation optimizations apply universally to all 4 database adapters (PostgreSQL, MariaDB, SQLite, MongoDB) as well as core CMS database tables:

  1. Copy-On-Write Request Body Sanitization (src/content/content-utils.ts):

    • validateFieldConstraints() and stripNullRows() use a Copy-On-Write pattern.
    • For 99% of normal write payloads, zero shallow object copies ({ ...data }) are allocated, eliminating memory churn before reaching any database adapter.
  2. Asynchronous Non-Blocking Microtask Audit Logger (src/services/security/audit-service.ts):

    • Uses static node:crypto imports during SSR module load and a serialized chainLock queue to guarantee cryptographic hash chain integrity under concurrent writes.
    • Database after-insert audit hooks operate via non-blocking microtasks (this.log(...).catch(...)).
    • Enables tamper-evident SHA-256 hash chaining in memory without delaying HTTP response latency.
  3. High-Efficiency Role & Permission Cache (src/databases/auth/permissions.ts):

    • Evaluates RBAC permissions using compiled Uint32Array role bitsets and 5-minute LRU permission caching.
    • Reuses mapped roleIds across cache reads and writes to eliminate duplicate array mapping allocations.

πŸ›‘οΈ Benchmark Integrity & Anti-Cheating Guarantees

SveltyCMS achieves sub-millisecond query execution (0.839ms findById) through architectural efficiency, not by cheating, bypassing validation, or short-circuiting production contracts:

Integrity Standard SveltyCMS Guarantee Why It Matters
Full Security Contract Parity 100% Active (Authentication, RBAC, Valibot validation, multi-tenant isolation, SHA-256 audit logging)* SveltyCMS runs the full production middleware stack on every request. (Note: Audit logging is physically bypassed only when explicitly configured with DISABLE_AUDIT_LOGS=true during bulk benchmark data generation)
Real Database I/O 10,000 real documents on native PostgreSQL 16 / SQLite / MariaDB / MongoDB No mock responses or dummy fallbacks. Queries hit real indexed database tables.
Zero ORM Overhead (Direct SQL Helpers) Uses native Drizzle SQL helpers & raw driver bindings rather than heavy ORM query builder abstractions Heavy ORMs (Drizzle ORM dynamic joins, Knex AST generators) consume high CPU per request. SveltyCMS bypasses ORM compilation tax to run raw parameterized SQL directly.
Verified SDK Parity 0.00% overhead verified between LocalCMS SDK and raw DB driver Accessing content via SDK matches raw database driver execution speed without proxy tax.

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.

High Performance Core vs Enterprise Compliance Mode (AUDIT_CHAIN_SYNC)

SveltyCMS supports two operational modes for audit logging and revision management:

  1. High Performance Core (Default: AUDIT_CHAIN_SYNC=false):

    • Database writes (INSERT / UPDATE) return sub-5ms latencies directly to the caller.
    • Crypto SHA-256 Merkle-tree hash chaining, content revision snapshotting, and L2 cache invalidation pattern purges are dispatched asynchronously via microtask background queues.
    • Eliminates all write-path CPU & IO penalties, outperforming Payload, Directus, and Strapi on write benchmarks.
  2. Enterprise Compliance Mode (AUDIT_CHAIN_SYNC=true):

    • Cryptographic SHA-256 audit chaining and revision creation run synchronously inside the database write transaction.
    • Ensures zero-latency audit durability and instant Merkle-tree verification for ISO 27001, SOC2, and EU GDPR regulated environments.
Metric High Performance Core (false) Enterprise Compliance (true) Impact
Create Latency ~4.20 ms 17.66 ms 4.2x faster writes
Update Latency ~4.80 ms 23.82 ms 5.0x faster updates
Mixed RPS 2,800+ req/s 1,987 req/s +41% throughput

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.



Compliance Disclaimer: Based on publicly available documentation as of August 2026. [EU Directive 2006/114/EC]

performanceoptimizationcachingindexingdatabasearchitecture
Was this page helpful?