Alexander Petrov

September 15, 2026

Representing application state using SQLite

Applications with strict latency and throughput requirements cannot query a database server for every request. The number of queries and network latency make this approach impractical.

The typical solution is to keep application state locally and refresh it periodically from a centralized source.

A textbook example is an ad auction server—an OpenRTB bidder—processing hundreds of thousands of requests per second. Each auction must complete within a tight latency budget—typically around 300 milliseconds, including the network round trip, while using a consistent and reasonably fresh view of campaigns, budgets, targeting rules and creatives.

Based on my experience building such services, one way to represent consistent application-wide state is an in-memory SQLite database.

This is especially useful in low-level languages like Rust or Go. SQL becomes a high-level DSL for working with state, while SQLite handles the low-level data structures and  operations.

The idea is simple: keep global mutable state in the local database.

Explicit state model

Application state is usually represented using arrays, dictionaries, sets and secondary indexes.

As the application grows, these structures become scattered across the codebase. Their relationships are implicit in the code responsible for keeping them synchronized.

With SQLite, the complete state model is concentrated in DDL.

Tables, columns, indexes and constraints have proper domain names. Instead of an obscure composition of primitive objects, it is possible to see what state exists, how  different parts are related and which invariants must hold. DDL becomes a compact description of the application state.

State consistency as SQL

The point is not to move all business logic into the database.

The point is to describe state consistency using a high-level relational model rather than manually composing low-level primitives.

SQL describes:
  - which records belong to the current state
  - how different parts of the state relate
  - which combinations are valid
  - how changes happen atomically
  - how the required subset is selected

Without SQL, the same model is often spread across several arrays, dictionaries and indexes. Every update must keep all of them synchronized and even more important, it obscures the intent.

Ports & Adapters

This approach works particularly well with Ports & Adapters.

The application talks to ports. Adapters contain queries and translate between the relational model and the application.
Low-level concerns like memory layout, indexing, synchronization and byte management do not poison the application logic.

Simple state synchronization

A background process pulls the latest snapshot from a centralized source and loads it into staging tables.
After validation, it activates the new snapshot in a single transaction—for example, by switching the active generation identifier.
Readers observe either the previous complete snapshot or the new complete snapshot. They never observe a partially refreshed state.

This replaces synchronization logic spread across primitive data structures and locks with one explicit transactional boundary. Failed refreshes stay isolated and could be  inspected or retried without affecting the active state.

Transparent state

Another important benefit is transparency.  Application state could be inspected using ordinary SQL rather than a debugger, memory dump or custom diagnostic code.  For maximum performance, the application could normally use an in-memory database. When inspection or issue reproduction is needed, the same schema and queries could use file- backed storage.

Persistence becomes a deployment choice rather than an architectural commitment.
A copied database file also provides a snapshot of the application state that could be inspected and replayed locally.

Replacing the queue

The same database could serve as a durable buffer for outgoing events.

Instead of publishing every event directly to Kafka, the application writes it into a local SQLite table. A background process periodically flushes pending events to  centralized storage and records successful delivery. In this case, file-backed SQLite makes more sense. Buffered events survive restarts, and application state changes could be committed in the same transaction as outgoing events.

SQLite does not replace Kafka when distributed retention, replay, fan-out or independent consumers are required. But quite often the actual requirement is just a durable local  buffer combined with centralized storage such as S3 or NFS.

The result is a distributed data pipeline without a distributed queue.

No part is the best part.

About Alexander Petrov


I build products for fun and profit.
web page