Skip to main content
More

SQLite Profiling

Profile SQLite statements and transactions in Rivet Actors using bounded metrics and sampled diagnostics.

Profiling helps you find slow queries, transaction contention, and unnecessary storage activity.

Logging slow queries

RivetKit logs slow or failed SQLite statements and transactions. Logs include the fingerprint, outcome, timing breakdown, and storage activity; statement logs also include rows and bytes, while transaction logs include the statement count.

Search actor logs for sampled SQLite operation profile for statements or sampled SQLite transaction profile for transactions.

Identify operations

Transaction names

Transaction names provide a stable identity for profiling transactions. Use a short, static name to correlate metrics.

Pass { name: "complete-order" } as the options argument to db.transaction().

Without a name, RivetKit falls back to a fingerprint of the transaction’s statement sequence. Branches and different loop counts can therefore produce separate fingerprints.

Statement fingerprints

RivetKit hashes each SQL statement exactly as provided. The fingerprint groups metrics without putting SQL text in a Prometheus label.

For example, repeated SELECT * FROM orders WHERE id = ? calls share one fingerprint regardless of the bound ID.

Find the SQL for a fingerprint

RivetKit logs the SQL statement or transaction name for each tracked fingerprint.

For example, suppose a Prometheus result contains fingerprint="select-a1b2c3d4e5f60718":

  1. Copy the fingerprint: select-a1b2c3d4e5f60718.

  2. Search the actor logs for sqlite fingerprint catalog and select-a1b2c3d4e5f60718.

  3. Read identity from the matching log line:

    sqlite fingerprint catalog fingerprint="select-a1b2c3d4e5f60718" identity="SELECT value FROM items WHERE id = ?"
    

Query metrics

Collect Prometheus metrics from each node to use the queries below. Some examples use optional metric families; see Configure profiling.

Slowest statements and transactions

Find the statements and transactions with the highest 95th-percentile latency.

histogram_quantile(
  0.95,
  sum by (le, actor_name, type, fingerprint) (
    rate(rivet_rivetkit_sqlite_duration_seconds_bucket[5m])
  )
)

Slowest latency phases

Break down slow operations to see whether they spend time waiting, executing SQL, or accessing storage.

  • transaction_wait: waiting for another transaction on the actor to finish.
  • worker_wait: waiting for earlier SQLite work on the actor to finish.
  • storage: loading or saving SQLite data.
  • local_work: executing SQL and preparing results, excluding storage time.
  • application_time: time the transaction stays open between SQL calls.
  • commit: saving changes at the end of a transaction.
histogram_quantile(
  0.95,
  sum by (le, actor_name, type, fingerprint, phase) (
    rate(rivet_rivetkit_sqlite_phase_duration_seconds_bucket[5m])
  )
)

Non-success outcomes

Find statements and transactions that fail, roll back, expire, or lose their connection.

sum by (actor_name, type, fingerprint, outcome) (
  rate(rivet_rivetkit_sqlite_outcome_total{outcome!="success"}[5m])
)

Transaction contention

See whether transactions are waiting for other transactions on the same actor.

max by (actor_name) (
  max_over_time(rivet_rivetkit_sqlite_coordinator_queue_depth[5m])
)

Native worker saturation

See whether SQLite operations are backing up on an actor. A sustained queue means work is arriving faster than SQLite can finish it, while worker_inflight shows how often SQLite is busy.

max by (actor_name) (
  max_over_time(rivet_rivetkit_sqlite_worker_queue_depth[5m])
)
avg by (actor_name) (
  avg_over_time(rivet_rivetkit_sqlite_worker_inflight[5m])
)

Transactions with the most statements

Find transactions that execute many SQL statements before finishing. Large counts can identify loops or oversized units of work; use a static transaction name to keep its fingerprint stable.

histogram_quantile(
  0.95,
  sum by (le, actor_name, fingerprint) (
    rate(rivet_rivetkit_sqlite_transaction_statement_count_bucket[5m])
  )
)

Average storage round trips per operation

See how many times each operation contacts storage on average. High counts can indicate a missing index, a large scan, or ineffective prefetching.

sum by (actor_name, type, fingerprint) (
  rate(rivet_rivetkit_sqlite_get_pages_round_trips_sum[5m])
)
/
sum by (actor_name, type, fingerprint) (
  rate(rivet_rivetkit_sqlite_get_pages_round_trips_count[5m])
)

Pages per physical storage request

See how many pages each storage request asks for and returns. Compare response_present with demand_requested to find response amplification; overflow_expansion_extra shows overflow-chain reads, and prefetch_requested shows speculative reads.

sum by (actor_name, request_ordinal, page_kind) (
  rate(rivet_rivetkit_sqlite_get_pages_pages_sum[5m])
)
/
sum by (actor_name, request_ordinal, page_kind) (
  rate(rivet_rivetkit_sqlite_get_pages_pages_count[5m])
)

Large storage responses

Find storage requests that return unusually large amounts of SQLite data.

histogram_quantile(
  0.95,
  sum by (le, actor_name, request_ordinal) (
    rate(rivet_rivetkit_sqlite_get_pages_response_bytes_bucket[5m])
  )
)

Missing response pages

Find storage requests that could not return every requested page.

sum by (actor_name, request_ordinal) (
  rate(rivet_rivetkit_sqlite_get_pages_missing_pages_total[5m])
)

SQLite page usage by kind

Break down the pages used for each kind of SQLite activity. High page counts can indicate a missing index or a large scan.

sum by (actor_name, type, page_kind) (
  rate(rivet_rivetkit_sqlite_local_pages_total[5m])
)

SQLite data volume by kind

Compare bytes used by query parameters, results, storage reads, and writes.

sum by (actor_name, type, byte_kind) (
  rate(rivet_rivetkit_sqlite_local_bytes_total[5m])
)

Configure profiling

By default, native SQLite records metrics for all statement and transaction fingerprints, up to the tracking limits. Each query gets a duration histogram with 10 ms, 100 ms, and 1 s boundaries, outcome counters, and a counter for pages returned from storage. Duration _count and _sum provide execution count and cumulative duration. Worker queue depth, worker activity, transaction queue depth, fingerprint overflow, and dropped diagnostic events are also enabled.

All other profiling metric families are disabled by default. This configuration surface is experimental.

For counters only, disable the duration histogram on your database provider:

profiling: {
  metrics: { durationHistogram: false },
}

This emits rivet_rivetkit_sqlite_operations_total and rivet_rivetkit_sqlite_duration_seconds_total instead of the duration histogram. Outcome counters and rivet_rivetkit_sqlite_pages_fetched_total remain enabled. Disabled metric families allocate no series and receive no observations.

To limit query metrics, set filterQueries (exact SQL) or filterQueryFingerprints. The filters are combined as a union; both empty or omitted means all queries. They do not suppress diagnostic logs or enabled actor-level storage metrics.

profiling: {
  filterQueries: ["SELECT value FROM items WHERE id = ?"],
  durationHistogramBucketsSeconds: [0.01, 0.1, 1],
  metrics: { localPages: true },
}

RIVETKIT_SQLITE_PROFILING overrides the filters with a JSON object containing filterQueries and/or filterQueryFingerprints. {} selects all queries. The former queries and fingerprints fields have been removed.

Duration bucket boundaries must be positive, finite, strictly increasing, and nonempty. All actor configurations enabling the duration histogram in one process must use the same boundaries; conflicting configurations are rejected. Other optional histograms retain their fixed boundaries.

The metrics switches are:

OptionDefaultScope
durationtruePer-query count and cumulative duration
durationHistogramtruePer-query duration buckets; requires duration
outcomestruePer-query counts by outcome, including errors
pagesFetchedtruePages returned from storage per statement, including returned prefetch pages
phaseDurationHistogramfalsePer-query timing phases
getPagesRoundTripsHistogramfalseStorage round trips per operation
transactionStatementCountHistogramfalseStatements per transaction
localPagesfalseActor-level page counters by kind
localBytesfalseActor-level byte counters by kind
getPagesDurationHistogramfalsePhysical storage request duration
getPagesPagesHistogramfalsePages per physical storage request
getPagesResponseBytesHistogramfalseBytes per physical storage response
getPagesMissingPagesfalseMissing response pages
workerQueueDepthtrueActor-level queued SQLite work
workerInflighttrueActor-level executing SQLite work
coordinatorQueueDepthtrueActor-level transaction waiters
fingerprintOverflowtrueObservations exceeding tracking limits
eventDroppedtrueDropped diagnostic events

The default limits are 512 statement fingerprints, 8 transaction fingerprints, and 25,000 profiling series per process. Series budgeting accounts for enabled metric families and configured duration buckets. Prometheus series remain allocated for the lifetime of the process. Increase limits only for useful operations that repeat regularly.

slowOperationThresholdMs, baselineSampleRate, and diagnostic event limits control logs, independently of metric selection. Sampling logs does not sample Prometheus observations.

Troubleshooting

Queries are missing

Check the query filters and rivet_rivetkit_sqlite_fingerprint_overflow_total. Once a fingerprint or series limit is reached, new per-query observations are dropped rather than creating unbounded series. Enable the optional metric families needed by the examples above.

Too many fingerprints

Statement fingerprints use the exact query text. Keep formatting and query structure static, and pass dynamic values as bindings instead of constructing SQL strings.

Transactions are hard to identify

Unnamed transactions are grouped by their statement sequence, which can vary across branches. Add a static name to each important transaction.

Storage activity is high

Use the storage queries above to compare page counts, response bytes, and round trips by actor name. Large scans or missing indexes are common causes.

Diagnostics are missing

Diagnostic events are sampled and bounded. Check rivet_rivetkit_sqlite_event_dropped_total for rate limiting or backpressure; aggregate Prometheus metrics continue reporting when events are dropped.

No profiling metrics appear

Confirm the relevant metric family is enabled and the query matches your filters. Profiling currently applies to native actor-local SQLite, not remote or wasm SQLite.

Edit this page Last updated November 10, 2026