---
title: "Incompatible API changes"
description: "Aerospike Rust client incompatible API changes guide for versions 1.0.0, 2.1.0, and 2.2.0 migrations."
---

# Incompatible API changes

> For the complete documentation index see: [llms.txt](https://aerospike.com/docs/llms.txt)
> 
> All documentation pages available in markdown.

This page lists non-backwards-compatible changes in the Aerospike Rust client, grouped by the version that introduced them. Sections are ordered newest first. If you’re upgrading across multiple versions, review and apply the changes for every version in between. For the minimum Aerospike server version required by each client release, see the [Client matrix](https://aerospike.com/docs/develop/client-matrix).

## Version 2.2.0: Batch and value-conversion behavior changes

Version 2.2.0 does not change any method signatures, but it changes the behavior of a few edge cases. Application code that depends on the old behavior needs updating even though it still compiles.

### Batch calls no longer fail entirely on a single bad key

A batch call with an unroutable key, or a per-key error on the last record in the response, used to return `Err` for the whole call. It now returns `Ok`, with the affected rows marked on the result (for example `Error::ServerError` or a partition-unavailable outcome on that row’s `BatchRecord`).

::: caution
This is a silent behavior change. Code that assumes an `Ok` result means every row in the batch succeeded now processes partial or missing data without raising an error. Update every `client.batch()` call site to check each row’s outcome before you upgrade to 2.2.0.
:::

Old assumption (no longer holds in 2.2.0):

```rust
match client.batch(&BatchPolicy::default(), &ops).await {

    Ok(results) => { /* every row succeeded */ }

    Err(err) => { /* a single bad key failed the whole call */ }

}
```

New: check each row’s outcome even when the call returns `Ok`.

```rust
match client.batch(&BatchPolicy::default(), &ops).await {

    Ok(results) => {

        for result in &results {

            match &result.record {

                Some(record) => println!("{:?} => {:?}", result.key, record.bins),

                None => println!("row failed or not found: {:?}", result.key),

            }

        }

    }

    Err(err) => { /* only when nothing in the batch was routable */ }

}
```

> 📖 **API reference**: [`Client::batch()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.batch) | [`BatchRecord`](https://docs.rs/aerospike/latest/aerospike/struct.BatchRecord.html)

Batch retries are also capped at `max_retries + 1` attempts. Previously the budget was `max_retries + 2`, and `max_retries = 0` did not disable retries; now `max_retries = 0` means no retry.

### `u64` values above `i64::MAX` panic instead of silently wrapping

`Value::from(u64)`, and therefore `as_key!` with a `u64` argument, panics for values above `i64::MAX`. Earlier versions silently wrapped the value to a negative integer instead, which could produce a different key or bin value than intended without any error.

::: caution
Aerospike’s wire protocol only supports signed 64-bit integers. A `u64` value that exceeds `i64::MAX` can reach this code path in production (for example, from an externally generated ID). Before upgrading, audit call sites that convert `u64` values to `Value` or use `as_key!` with a `u64` argument. If truncation to `i64` is not acceptable for your data, store the value as a string or byte array instead of an integer.
:::

If truncation is intentional, cast explicitly to make it visible in the code:

```rust
let v = Value::from(large_u64 as i64);
```

### `Value::Infinity` and `Value::Wildcard` return an error instead of ending the process

Using `Value::Infinity` or `Value::Wildcard` as a bin value or record key now returns `Error::InvalidArgument` instead of ending the process. Both values are valid only as collection data type (CDT) range bounds.

### New `Error::MaxErrorRate` variant

Setting `ClientPolicy.max_error_rate` and `error_rate_window` enables a per-node circuit breaker; commands to a node that has tripped it fail fast with the new `Error::MaxErrorRate` variant. Match on it if you distinguish circuit-breaker failures from other errors. See [Error handling](https://aerospike.com/docs/develop/client/rust/2.2.0/error-handling) for the full variant list.

## Version 2.1.0: Async runtime, unified query, new batch and error APIs

Version 2.1.0 is a major rewrite of the Rust client. All database operations are now async, the scan and query APIs are unified, batch operations support writes and deletes, and the error model has been replaced with a flat enum.

### Cargo.toml dependency changes

v2.1.0 uses either the `tokio` or `async-std` runtime crate for its asynchronous operations, depending on which Cargo feature you enable. It also uses the `futures` crate for stream iteration.

Old:

```toml
[dependencies]

aerospike = "1.3"
```

New:

```toml
[dependencies]

aerospike = "2.1.0"

tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

futures = "0.3"
```

> 📖 **API reference**: [`Client`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html)

### All Client methods are async

When using Tokio, every `Client` method that performs I/O is now an `async fn` and must use `.await` inside a Tokio runtime. Standalone programs require `#[tokio::main]`.

If you can’t migrate call sites to `async`/`await` yet, enable the client’s `sync` feature instead to keep the blocking API. See [Install](https://aerospike.com/docs/develop/client/rust/2.2.0/install#install) for the required Cargo feature flags.

Old:

```rust
fn main() {

    let client = Client::new(&ClientPolicy::default(), &hosts)

        .expect("Failed to connect");

    client.put(&wpolicy, &key, &bins).unwrap();

    let record = client.get(&rpolicy, &key, Bins::All).unwrap();

    client.close().unwrap();

}
```

New:

```rust
#[tokio::main]

async fn main() -> Result<(), Box<dyn std::error::Error>> {

    let client = Client::new(&ClientPolicy::default(), &hosts).await?;

    client.put(&wpolicy, &key, &bins).await?;

    let record = client.get(&rpolicy, &key, Bins::All).await?;

    client.close().await?;

    Ok(())

}
```

> 📖 **API reference**: [`Client::new()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.new) | [`Client::close()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.close)

With async-std, the usage is:

```rust
use async_std::task;

pub async fn my_async_main() -> Result<(), Box<dyn std::error::Error>> {

    // ...

    Ok(())

}

pub fn main() -> Result<(), Box<dyn std::error::Error>> {

    task::block_on(my_async_main())

}
```

See the [async-std documentation](https://crates.io/crates/async-std) for more help.

### Macro imports

The `#[macro_use] extern crate aerospike` pattern is no longer needed. Import macros explicitly.

Old:

```rust
#[macro_use]

extern crate aerospike;

use aerospike::*;
```

New:

```rust
use aerospike::{as_key, as_bin, as_val, Client, ClientPolicy, ReadPolicy, WritePolicy};
```

> 📖 **API reference**: [`as_key!`](https://docs.rs/aerospike/latest/aerospike/macro.as_key.html) | [`as_bin!`](https://docs.rs/aerospike/latest/aerospike/macro.as_bin.html) | [`as_val!`](https://docs.rs/aerospike/latest/aerospike/macro.as_val.html)

### Error model: error-chain replaced by Error enum

The v1.3.0 error-chain pattern `Error(ErrorKind::..., _)` is replaced by a flat `aerospike::Error` enum with named variants.

Old:

```rust
use aerospike::errors::{Error, ErrorKind};

match client.get(&rpolicy, &key, Bins::All) {

    Ok(record) => println!("{}", record),

    Err(Error(ErrorKind::ServerError(ResultCode::KeyNotFoundError), _)) => {

        println!("Not found");

    }

    Err(err) => println!("Error: {}", err),

}
```

New:

```rust
use aerospike::{Error, ResultCode};

match client.get(&rpolicy, &key, Bins::All).await {

    Ok(record) => println!("{}", record),

    Err(Error::ServerError(ResultCode::KeyNotFoundError, ..)) => {

        println!("Not found");

    }

    Err(err) => println!("Error: {}", err),

}
```

The following `Error` variants are available in v2.1.0:

| Variant | Description |
| --- | --- |
| `ServerError(ResultCode, bool, String)` | Server returned an error result code |
| `Connection(String)` | Cluster connection failure |
| `Timeout(String)` | Client-side timeout |
| `InvalidArgument(String)` | Invalid method argument |
| `InvalidNode(String)` | Node not found or unavailable |
| `NoMoreConnections` | Connection pool exhausted |
| `ClientError(String)` | General client-side error |
| `Io(std::io::Error)` | Underlying I/O error |

> 📖 **API reference**: [`Error`](https://docs.rs/aerospike/latest/aerospike/enum.Error.html) | [`ResultCode`](https://docs.rs/aerospike/latest/aerospike/enum.ResultCode.html)

### Scan and query unified into `client.query()`

`client.scan()` and `ScanPolicy` have been removed. Use `client.query()` with a `Statement` that has no secondary index filter and `PartitionFilter::all()`.

Old:

```rust
use aerospike::ScanPolicy;

let policy = ScanPolicy::default();

match client.scan(&policy, "test", "demo", Bins::All) {

    Ok(records) => {

        for record in &*records {

            match record {

                Ok(rec) => println!("{}", rec),

                Err(err) => eprintln!("Error: {}", err),

            }

        }

    }

    Err(err) => eprintln!("Scan failed: {}", err),

}
```

New:

```rust
use aerospike::{QueryPolicy, Statement, Bins, PartitionFilter};

use futures::stream::StreamExt;

let policy = QueryPolicy::default();

let stmt = Statement::new("test", "demo", Bins::All);

let rs = client.query(&policy, PartitionFilter::all(), stmt).await?;

let mut stream = rs.into_stream();

while let Some(result) = stream.next().await {

    match result {

        Ok(record) => println!("{}", record),

        Err(err) => eprintln!("Error: {}", err),

    }

}
```

The `client.scan_node()` and `client.query_node()` methods have also been removed. Use `PartitionFilter` to target specific partitions instead.

> 📖 **API reference**: [`Client::query()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query) | [`Statement::new()`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html#method.new) | [`PartitionFilter`](https://docs.rs/aerospike/latest/aerospike/struct.PartitionFilter.html)

### Query signature change: `PartitionFilter` parameter added

`client.query()` now requires a `PartitionFilter` argument between the policy and the statement. This enables partition-based pagination.

Old:

```rust
let rs = client.query(&QueryPolicy::default(), stmt)?;
```

New:

```rust
let rs = client.query(&QueryPolicy::default(), PartitionFilter::all(), stmt).await?;
```

> 📖 **API reference**: [`Client::query()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query) | [`PartitionFilter::all()`](https://docs.rs/aerospike/latest/aerospike/struct.PartitionFilter.html#method.all)

### Recordset iteration: `for` loop replaced by async stream

v1.3.0 returned `Arc<Recordset>` which implemented `IntoIterator` for synchronous `for` loops. v2.1.0 requires converting the `Recordset` into an async `RecordStream` and consuming it with `StreamExt::next()`.

Old:

```rust
let records = client.query(&policy, stmt)?;

for record in &*records {

    match record {

        Ok(rec) => println!("{}", rec),

        Err(err) => eprintln!("Error: {}", err),

    }

}
```

New:

```rust
use futures::stream::StreamExt;

let rs = client.query(&policy, PartitionFilter::all(), stmt).await?;

let mut stream = rs.into_stream();

while let Some(result) = stream.next().await {

    match result {

        Ok(record) => println!("{}", record),

        Err(err) => eprintln!("Error: {}", err),

    }

}
```

> 📖 **API reference**: [`Recordset::into_stream()`](https://docs.rs/aerospike/latest/aerospike/struct.Recordset.html#method.into_stream)

### Batch API: `batch_get()` replaced by `batch()`

The v1.3.0 read-only `client.batch_get(Vec<BatchRead>)` is replaced by the v2.1.0 `client.batch(&BatchPolicy, &[BatchOperation])`, which supports reads, writes, deletes, and UDF calls in a single batch.

Old:

```rust
use aerospike::BatchRead;

let bins = Bins::from(["name", "age"]);

let mut batch_reads = vec![];

for i in 0..10 {

    let key = as_key!("test", "test", i);

    batch_reads.push(BatchRead::new(key, &bins));

}

match client.batch_get(&BatchPolicy::default(), batch_reads) {

    Ok(results) => {

        for result in &results {

            match &result.record {

                Some(record) => println!("{:?} => {:?}", result.key, record.bins),

                None => println!("Not found: {:?}", result.key),

            }

        }

    }

    Err(err) => eprintln!("Batch error: {}", err),

}
```

New:

```rust
use aerospike::{BatchPolicy, BatchOperation, BatchReadPolicy, Bins};

let bins = Bins::from(["name", "age"]);

let bpr = BatchReadPolicy::default();

let ops: Vec<BatchOperation> = (0..10)

    .map(|i| BatchOperation::read(&bpr, as_key!("test", "test", i), bins.clone()))

    .collect();

match client.batch(&BatchPolicy::default(), &ops).await {

    Ok(results) => {

        for result in &results {

            match &result.record {

                Some(record) => println!("{:?} => {:?}", result.key, record.bins),

                None => println!("Not found: {:?}", result.key),

            }

        }

    }

    Err(err) => eprintln!("Batch error: {}", err),

}
```

New batch operation types:

| v2.1.0 method | Description |
| --- | --- |
| `BatchOperation::read()` | Read bins from a record |
| `BatchOperation::read_ops()` | Read with specific operations |
| `BatchOperation::write()` | Write operations to a record |
| `BatchOperation::delete()` | Delete a record |
| `BatchOperation::udf()` | Execute a UDF on a record |

Each operation type has its own policy: `BatchReadPolicy`, `BatchWritePolicy`, `BatchDeletePolicy`, `BatchUDFPolicy`.

> 📖 **API reference**: [`Client::batch()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.batch) | [`BatchOperation`](https://docs.rs/aerospike/latest/aerospike/enum.BatchOperation.html) | [`BatchRecord`](https://docs.rs/aerospike/latest/aerospike/struct.BatchRecord.html)

### Bin passing: reference wrappers removed from `put()`, `add()`, `append()`, `prepend()`

v1.3.0 accepted bins by reference wrapper (`&[A] where A: AsRef<Bin<'b>>`). v2.1.0 accepts `&[Bin]` directly, so bins no longer need to be wrapped in an extra `&`.

Old:

```rust
let bin = as_bin!("name", "Alice");

client.put(&wpolicy, &key, &vec![&bin])?;
```

New:

```rust
let bin = as_bin!("name", "Alice");

client.put(&wpolicy, &key, &[bin]).await?;
```

> 📖 **API reference**: [`Client::put()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.put)

### `exists()` takes `ReadPolicy` instead of `WritePolicy`

Old:

```rust
let exists = client.exists(&WritePolicy::default(), &key)?;
```

New:

```rust
let exists = client.exists(&ReadPolicy::default(), &key).await?;
```

> 📖 **API reference**: [`Client::exists()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.exists)

### Index creation: `create_index()` replaced by `create_index_on_bin()`

The v1.3.0 methods `create_index()` and `create_complex_index()` are replaced by `create_index_on_bin()`, which combines both and adds support for CDT context.

Old:

```rust
let task = client.create_index(

    &WritePolicy::default(), "test", "demo", "age",

    "idx_demo_age", IndexType::Numeric,

)?;
```

New:

```rust
use aerospike::{AdminPolicy, IndexType, CollectionIndexType, Task};

let task = client.create_index_on_bin(

    &AdminPolicy::default(),

    "test", "demo", "age", "idx_demo_age",

    IndexType::Numeric,

    CollectionIndexType::Default,

    None,

).await?;

task.wait_till_complete(None).await?;
```

v2.1.0 also adds `create_index_using_expression()` for expression-based secondary indices.

> 📖 **API reference**: [`Client::create_index_on_bin()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.create_index_on_bin) | [`Client::create_index_using_expression()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.create_index_using_expression)

### `drop_index()` returns `DropIndexTask`

Old:

```rust
client.drop_index(&WritePolicy::default(), "test", "demo", "idx_demo_age")?;
```

New:

```rust
let task = client.drop_index(

    &AdminPolicy::default(), "test", "demo", "idx_demo_age",

).await?;
```

> 📖 **API reference**: [`Client::drop_index()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.drop_index) | [`DropIndexTask`](https://docs.rs/aerospike/latest/aerospike/struct.DropIndexTask.html)

### Admin operations take `AdminPolicy`

`register_udf()`, `remove_udf()`, `truncate()`, `create_index_on_bin()`, and `drop_index()` accept `&AdminPolicy` instead of `&WritePolicy`.

Old:

```rust
let task = client.register_udf(

    &WritePolicy::default(), code.as_bytes(), "example.lua", UDFLang::Lua,

)?;
```

New:

```rust
use aerospike::Task;

let task = client.register_udf(

    &AdminPolicy::default(), code.as_bytes(), "example.lua", UDFLang::Lua,

).await?;

task.wait_till_complete(None).await?;
```

> 📖 **API reference**: [`Client::register_udf()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.register_udf) | [`AdminPolicy`](https://docs.rs/aerospike/latest/aerospike/struct.AdminPolicy.html)

### `remove_udf()` simplified and returns `UdfRemoveTask`

The `language` parameter has been removed, and the method returns a task that can be awaited.

Old:

```rust
client.remove_udf(&WritePolicy::default(), "example.lua", UDFLang::Lua)?;
```

New:

```rust
let task = client.remove_udf(&AdminPolicy::default(), "example.lua").await?;
```

> 📖 **API reference**: [`Client::remove_udf()`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.remove_udf) | [`UdfRemoveTask`](https://docs.rs/aerospike/latest/aerospike/struct.UdfRemoveTask.html)

### Thread-based concurrency replaced by async tasks

v1.3.0 used `std::thread` with `Arc<Client>`. v2.1.0 uses `tokio::spawn` with `Arc<Client>`, since `Client` implements `Send + Sync`.

Old:

```rust
use std::sync::Arc;

use std::thread;

let client = Arc::new(Client::new(&cpolicy, &hosts)?);

let mut threads = vec![];

for i in 0..4 {

    let client = client.clone();

    threads.push(thread::spawn(move || {

        client.put(&wpolicy, &key, &bins).unwrap();

    }));

}

for t in threads {

    t.join().unwrap();

}
```

New:

```rust
use std::sync::Arc;

let client = Arc::new(Client::new(&cpolicy, &hosts).await?);

let mut handles = vec![];

for i in 0..4 {

    let client = client.clone();

    handles.push(tokio::spawn(async move {

        client.put(&wpolicy, &key, &bins).await.unwrap();

    }));

}

futures::future::join_all(handles).await;
```

> 📖 **API reference**: [`Client`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html)

### TLS support added using `rustls`

v2.1.0 adds optional TLS support through the `tls` Cargo feature. Configure TLS by setting `ClientPolicy.tls_config` to a `rustls::ClientConfig`.

```toml
[dependencies]

aerospike = { version = "2.1.0", features = ["tls"] }

rustls = "0.23"
```

```rust
let mut policy = ClientPolicy::default();

policy.tls_config = Some(rustls_client_config);

let client = Client::new(&policy, &"tls-host.example.com:4333").await?;
```

> 📖 **API reference**: [`ClientPolicy`](https://docs.rs/aerospike/latest/aerospike/struct.ClientPolicy.html)

### Summary of removed and renamed types

| v1.3.0 | v2.1.0 | Notes |
| --- | --- | --- |
| `ScanPolicy` | `QueryPolicy` | Scans use `client.query()` with no filter |
| `client.scan()` | `client.query()` | Pass `PartitionFilter::all()` for full-set scan |
| `client.scan_node()` | Removed | Use `PartitionFilter` to scope by partition |
| `client.query_node()` | Removed | Use `PartitionFilter` to scope by partition |
| `client.batch_get()` | `client.batch()` | Accepts `&[BatchOperation]` |
| `BatchRead` | `BatchRecord` (result) / `BatchOperation` (input) | Supports read, write, delete, UDF |
| `client.create_index()` | `client.create_index_on_bin()` | Adds `CollectionIndexType` and `ctx` params |
| `client.create_complex_index()` | `client.create_index_on_bin()` | Merged into single method |
| `errors::Error` (struct) | `Error` (enum) | Flat enum, no error-chain |
| `errors::ErrorKind` | Removed | Variants moved to `Error` enum directly |
| `for record in &*records` | `rs.into_stream()` + `stream.next().await` | Requires `futures::stream::StreamExt` |

### Summary of new types in v2.1.0

| Type | Description |
| --- | --- |
| `PartitionFilter` | Partition-based cursor for query/scan pagination |
| `BatchOperation` | Batch operation enum (read, write, delete, UDF) |
| `BatchReadPolicy` | Per-operation policy for batch reads |
| `BatchWritePolicy` | Per-operation policy for batch writes |
| `BatchDeletePolicy` | Per-operation policy for batch deletes |
| `BatchUDFPolicy` | Per-operation policy for batch UDF calls |
| `BatchRecord` | Result record from a batch operation |
| `DropIndexTask` | Async task for tracking index deletion |
| `UdfRemoveTask` | Async task for tracking UDF removal |
| `QueryDuration` | Hint for expected query duration |
| `ReadTouchTTL` | Controls read-based TTL refresh behavior |
| `Recordset::into_stream()` | Converts recordset to async `RecordStream` |

---

## Version 1.0.0

### Replace predicate expressions with Aerospike Expressions

The old predicate expression functionality has been replaced by [Aerospike Expressions](https://aerospike.com/docs/develop/expressions). Aerospike Expressions include all previous predicate expression functionality (with a different, improved syntax) plus support for record metadata expressions and list/map/bit/hyperloglog expression methods analogous to those used in operations.

This version of the client requires server version 5.2.0.4+. Code using old PredExp must be converted to the new syntax. Here is an example:

Old:

```rust
let mut statement = Statement::new(namespace, &set_name, Bins::All);

statement.add_predicate(as_predexp_integer_bin!("age".to_string()));

statement.add_predicate(as_predexp_integer_value!(35));

statement.add_predicate(as_predexp_integer_lesseq!());

let qpolicy = QueryPolicy::default();

let results = client.query(&qpolicy, statement)?;
```

> 📖 **API reference**: [`Statement::new`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html) | [`QueryPolicy::default`](https://docs.rs/aerospike/latest/aerospike/struct.QueryPolicy.html) | [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query)

New:

```rust
use aerospike::expressions as exp;

let mut qpolicy = QueryPolicy::default();

qpolicy.base_policy.filter_expression = Some(

    exp::le(

        exp::int_bin("age".to_string()),

        exp::int_val(35)

    )

);

let statement = Statement::new(namespace, &set_name, Bins::All);

let results = client.query(&qpolicy, PartitionFilter::all(), statement).await?;
```

> 📖 **API reference**: [`Statement::new`](https://docs.rs/aerospike/latest/aerospike/struct.Statement.html) | [`QueryPolicy::default`](https://docs.rs/aerospike/latest/aerospike/struct.QueryPolicy.html) | [`Client::query`](https://docs.rs/aerospike/latest/aerospike/struct.Client.html#method.query)

## Next steps

-   [Install](https://aerospike.com/docs/develop/client/rust/2.2.0/install): Cargo features and minimum Rust version for each client release.
-   [Connecting](https://aerospike.com/docs/develop/client/rust/2.2.0/connect): Set up a `Client` after upgrading.
-   [Error handling](https://aerospike.com/docs/develop/client/rust/2.2.0/error-handling): Match on the current `aerospike::Error` enum.
-   [Client matrix](https://aerospike.com/docs/develop/client-matrix): Minimum Aerospike server version for each client release.