Runtime and observability
When a request is slow, you want to see its database work alongside the rest of that request. When traffic grows, you want requests to share a bounded number of database connections.
Rent uses your application's tracing subscriber and a shared connection pool.
Start with query tracing, then tune connections and timeouts for your deployment.
See database work in a request trace
Enable Rent's optional instrumentation Cargo feature when you want detailed
measurement. It is off by default, including in debug builds. Add it to your
existing backend features:
features = ["postgres", "instrumentation"]With that feature, Rent emits spans for connections, transactions, queries, and migrations. They include the database dialect, operation, bind count, and statement length without recording bound values.
Install your preferred subscriber in the application:
tracing_subscriber::fmt()
.with_env_filter("my_app=info,rent_driver=debug,rent_migrate=info")
.try_init()?;Without the feature, detailed Rent spans are compiled out and Rent-created pools turn off SQLx statement logging. Application logging and ordinary Rent warnings and errors remain available. An environment filter cannot enable measurement code that was excluded from the build. Rent installs no subscriber or exporter.
Cargo combines dependency features. An explicit instrumentation request elsewhere
in the same application build enables it for the shared driver. Inspect the actual
build with cargo tree -e features -i rent-driver, or check
rent::driver::INSTRUMENTATION_ENABLED. Keep diagnostic tools in separate builds.
Pools created by your application and wrapped in DatabasePool retain their SQLx
logging settings. This feature controls Rent's spans and logging on connections
Rent creates. It does not remove internal timing inside SQLx or turn off logging
owned by another library.
Share a bounded connection pool
Configure the pool explicitly for production workloads:
use std::time::Duration;
use rent::driver::{DatabasePool, PoolOptions};
use rent::sql::Dialect;
let pool = DatabasePool::connect_with(
Dialect::Postgres,
&database_url,
PoolOptions {
max_connections: 32,
min_connections: 4,
acquire_timeout: Duration::from_secs(3),
idle_timeout: Some(Duration::from_secs(10 * 60)),
max_lifetime: Some(Duration::from_secs(30 * 60)),
test_before_acquire: true,
},
)
.await?;DatabasePool::connect uses conservative defaults when an application does not need custom settings.
Set PostgreSQL timeouts
PostgreSQL applications can configure session timeouts without changing their generated client:
use rent::driver::PostgresSessionOptions;
let pool = DatabasePool::connect_postgres_with(
&database_url,
PoolOptions::default(),
PostgresSessionOptions {
statement_timeout: Some(Duration::from_secs(5)),
lock_timeout: Some(Duration::from_secs(1)),
idle_in_transaction_session_timeout: Some(Duration::from_secs(30)),
},
).await?;
let root = generated::rent::RootClient::new(pool)?;The final ? applies to access-enabled schemas. Ordinary schemas return the client directly from new(pool).
This API requires the postgres Cargo feature. Existing connect_with calls and PoolOptions stay unchanged.
Settings travel in every physical connection's startup parameters, including replacement connections.
There is no timeout-initialization statement on each query or checkout.
None preserves the URL/server setting. An explicit duration overrides the matching URL setting, and zero
disables it. Positive fractional milliseconds round up. Values above 2,147,483,647 milliseconds are rejected.
Unrelated URL options are retained. Proxies and transaction poolers must accept/handle these startup parameters.
Otherwise use their documented server/account configuration instead. Rent reports setup failures directly.
statement_timeout bounds each statement. lock_timeout bounds each lock wait. The idle-in-transaction
timeout terminates a session waiting for its next command inside a transaction. These do not bound the whole
request. Roll back an aborted transaction before reuse. A terminated connection must be replaced by the pool.
Keep connections available
Create the client once and share or clone it across handlers. Cloned clients reuse the same pool. They do not
open a pool per request. Acquiring a connection leases an idle connection when one is available and usable.
The pool opens connections as needed up to max_connections, then callers wait for a lease. A transaction
retains one connection until it commits or rolls back. Keep test_before_acquire enabled to validate idle
connections before reuse.
Pass transaction-bound clients into helpers for one coherent database unit. Do not retain a connection while waiting for an external API, Redis, a model response or user input. Applications can bound background admission with a semaphore before acquiring connections. A separate worker pool is optional isolation and adds to the total database connection budget across server instances.
Count the right layer
Read/write interceptors count logical application operations. Additional SQL may run for permission evaluation,
eager relationships, transaction setup, savepoints and retries. to_sql() inspects the root statement. It is not
an execution transcript. Driver spans can nest, so summing spans also overcounts some work.
With instrumentation enabled, Rent-created pools emit SQLx sqlx::query events for executed query paths, including raw SQL and internal permission queries.
An application-owned subscriber can count those events without retaining their SQL fields. The
runtime tutorial contains an executable example.
These events are still not network-round-trip or PostgreSQL protocol-message counts. Transaction cleanup,
connection health traffic and SQLx internals may use different paths. For an exact server-command inventory,
correlate selected requests with statement logs in an isolated database, including BEGIN/COMMIT/ROLLBACK and
retries. Full SQLx query logging can contain literal SQL text. Avoid exporting it or its summary field as
automatically redacted production telemetry.
See production access patterns for projections, batched counts, permission-safe pagination and short transactions together.
Concurrent SQLite writers
Use an isolated database file for SQLite tests with concurrent writers. Shared-cache in-memory databases
have different table-locking rules and can return SQLITE_LOCKED when multiple connections contend.
See SQLite's shared-cache locking rules for those semantics.
Retry safe units of work
For transactions that can safely be repeated after a serialization failure or deadlock, use the generated retry helper:
let post = client
.transaction_retry(3, |tx| async move {
tx.create_post()
.title("A retryable write")
.save()
.await
})
.await?;Rent retries only database errors classified as serialization conflicts, deadlocks, or backend lock contention.
That classification applies to transaction setup, the operation, and commit. Validation, authorization, application,
and rollback failures return immediately. Retry warnings include the completed attempt, total attempt budget, and
chosen backoff delay without recording query values. TransactionRetryPolicy controls the delay and jitter.
