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 worker to use the queries below.

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

Profiling is enabled by default and most applications do not need to configure it. The entire profiling configuration surface is experimental and subject to change without notice. Set profiling.slowOperationThresholdMs or profiling.baselineSampleRate on the database provider when needed.

Increase fingerprint limits only when other is hiding frequently repeated operations. Prometheus series remain allocated for the life of the process after admission.

Troubleshooting

Most results are other

Fast statements initially appear under other, while overflow metrics show when a fingerprint limit was reached. Increase limits only for useful operations that repeat regularly.

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 profiling was not disabled in the database provider. Profiling currently applies to native actor-local SQLite, not remote or wasm SQLite.