Skip to content

Best practices

For the complete documentation index see: llms.txt

All documentation pages available in markdown.

This page applies to Rust client version 3.0.0.

Sharing and reusing the client

Reuse a single client instance

The Aerospike client is Send and Sync (thread-safe). Create one client per cluster and reuse it for the lifetime of your application.

  • Do: Create the client once (such as at startup) and pass references (&Client) or wrap in Arc<Client> when sharing across threads or async tasks.
  • Don’t: Create a new Client for every request or per thread. Each client manages its own connection pool and cluster state. Multiple clients add unnecessary overhead and connection churn.

Single client in a multi-threaded or async environment

Because Client is Send + Sync, you can:

  • Share a reference across threads (for example, Arc<Client>).
  • Use the same client from many concurrent async tasks (for example, &Client in handlers).
  • Call client methods from a thread pool or a multi-threaded Tokio runtime; the client uses internal synchronization for connection and cluster state.

Example: one client, many concurrent tasks (async)

When you spawn tasks with tokio::spawn, the future’s output must be Send. Use Box<dyn std::error::Error + Send + Sync> (not bare Box<dyn Error>) if spawned tasks return errors:

#[macro_use]
extern crate aerospike;
use std::sync::Arc;
use aerospike::{Bins, Client, ClientPolicy, ReadPolicy};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let policy = ClientPolicy::default();
let client = Client::new(&policy, &"127.0.0.1:3000").await?;
let client = Arc::new(client);
// Spawn multiple tasks using the same client
let mut handles = vec![];
for i in 0..10 {
let client = Arc::clone(&client);
handles.push(tokio::spawn(async move {
let key = as_key!("test", "myset", i);
client.get(&ReadPolicy::default(), &key, Bins::All).await
}));
}
for h in handles {
let _ = h.await?;
}
// After spawned tasks finish, recover the client and close it.
if let Ok(client) = Arc::try_unwrap(client) {
client.close().await?;
}
Ok(())
}

Use if let Ok(client) = Arc::try_unwrap(client), not .expect(), because Client does not implement Debug.

📖 API reference: Client::new | Client::get | Client::close

Example: sync client with multiple threads

With the sync feature, use the same pattern: one Client (for example inside Arc) and clone the Arc for each thread. The sync client runs cluster tending on an internal runtime, so call sites do not need .await:

#[macro_use]
extern crate aerospike;
use std::sync::Arc;
use std::thread;
use aerospike::{Bins, Client, ClientPolicy, ReadPolicy};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(&ClientPolicy::default(), &"127.0.0.1:3000")?;
let client = Arc::new(client);
let mut handles = vec![];
for i in 0..4 {
let client = Arc::clone(&client);
handles.push(thread::spawn(move || {
let key = as_key!("test", "myset", i);
client.get(&ReadPolicy::default(), &key, Bins::All)
}));
}
for h in handles {
let _ = h.join().unwrap()?;
}
Ok(())
}

📖 API reference: Client::new | Client::get

When to create a new client

Create a new client only when:

  • Connecting to a different cluster (for example, another environment or region).
  • You have reconfigured the cluster (for example, new seed list) and need a fresh connection pool and cluster view.
  • The previous client has been closed and you need to reconnect.

For normal request handling, use one shared client and reuse it.

User-defined keys and storage

Aerospike identifies records by a digest (a hash of namespace, set, and your key). By default, only this digest is stored; the original user key (for example, "user_123" or 42) is not stored on the server. Single-record reads still work because your application already has the key and the client recomputes the digest.

If you need the key to be stored and returned (for example, in scan/query results or when iterating), use one of these:

  • Set WritePolicy.send_key to true: The client sends the user key to the server on every write. The server stores it with the record and returns it on reads, scans, and queries. Use this when you want the key available everywhere (scans, queries, secondary indexes) without putting it in a bin.

  • Store the key in a bin: Write the key as normal bin data (for example, a bin named "user_key") and read it back when you read the record. The server still identifies the record by digest; the key is application data only. Use this when you prefer to manage the key explicitly in your schema or need a different shape than the primary key.

Batch operations on one record: operate()

Use Client::operate() to run multiple operations (for example, add, get, put) on the same record in a single round-trip. The server runs the ops in order and returns the result (for example, bins from read ops).

Example: add then get in one call

use aerospike::operations;
use aerospike::{Client, ClientPolicy, WritePolicy};
// One round-trip: add 10 to "counter", then read "counter" back.
let add_op = as_bin!("counter", 10i64);
let ops = vec![operations::add(&add_op), operations::get_bin("counter")];
let rec = client.operate(&wpolicy, &key, &ops).await?;
// rec.bins contains the result of the get

Use operations::get_bin("bin_name") to read one bin after an increment. operations::get() takes no arguments and returns all bins from the record. Do not pass a bin name to get().

📖 API reference: Client::operate | operations::add | operations::get_bin | operations::get

Replace mode for full-record overwrites

When you create or update all bins for a record in one command, use Replace mode so the server can overwrite the record without reading it first. Do not use Replace when you are only updating a subset of bins, or you will remove bins you didn’t send.

let mut policy = WritePolicy::default();
policy.record_exists_action = RecordExistsAction::Replace;
client.put(&policy, &key, &bins).await?;

📖 API reference: WritePolicy::default | RecordExistsAction | Client::put

Reuse policies

Each command takes a policy as the first argument. If the policy is the same for a group of commands, create it once and reuse it instead of creating a new policy for each call.

let mut policy = WritePolicy::default();
policy.record_exists_action = RecordExistsAction::Replace;
client.put(&policy, &key1, &bins1).await?;
client.put(&policy, &key2, &bins2).await?;

📖 API reference: WritePolicy::default | RecordExistsAction | Client::put

Tune the connection pool and circuit breaker

When every connection in a node’s pool is in flight, a command paces and waits for one to be returned (bounded by the command deadline) instead of failing fast. Size ClientPolicy.max_conns_per_node for your workload’s concurrency to avoid relying on this pacing behavior for normal traffic.

Set ClientPolicy.max_error_rate and error_rate_window to tune the per-node circuit breaker: once a node’s error rate exceeds the threshold within the window, commands to that node fail fast with ErrorKind::MaxErrorRate instead of waiting out the full timeout. The circuit breaker is enabled by default (max_error_rate is 100 and error_rate_window is 1). Set max_error_rate to 0 to disable it.

let mut policy = ClientPolicy::default();
policy.max_error_rate = 200;
policy.error_rate_window = 5;

📖 API reference: ClientPolicy

Error handling

Client methods return Result<T, aerospike::Error>. Error is an opaque struct in 3.0. Use accessor methods such as server_result_code() or Error::kind() rather than matching Error::Variant directly. For propagation with ?, anyhow or thiserror, and matching on specific failures, see Error handling.

Server error details

Leave error_detail_verbosity at its default (0) in steady-state production. Enable it (1 for subcodes, higher only while debugging) when you need to dispatch on failure subtype, view detailed error messages, or trace an expression filter. Use only 0 through 3: the client does not reject or cap larger values, and they wrap (for example, 4 turns error details off). See Server error details and the concept page.

Next steps

Usage examples

Ready for more code examples? We have usage examples for all the basic CRUD operations.

Create records →

Error handling

Actionable errors with recovery suggestions. Know exactly what went wrong and how to fix it.

Error handling →