Library / SDK
elastic/elasticsearch-rs avatar
elastic/elasticsearch-rs

elasticsearch-rs: the official Rust client, generated from the REST specs

Official Elasticsearch Rust Client

768 stars85 forksRustApache-2.0

At a glance

What is it?
The official Rust client for Elasticsearch generates its API surface from the same REST API specifications the server uses, which is why its namespaced clients match the server's own grouping. It is also still in an alpha state, it is async only, and its most consequential configuration choice is which TLS backend you enable at compile time.
Who is it for?
Adopt elasticsearch-rs if you are writing a Rust service that talks to Elasticsearch and want typed request builders plus async support without hand-writing HTTP calls. Do not adopt it for a project that cannot tolerate an alpha client, because the README still describes the project as very much a work in progress and in an alpha state.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What it is, and the alpha caveat that comes first

The elasticsearch-rs repository is the official Rust client for Elasticsearch, published to crates.io as elasticsearch with full documentation on docs.rs. The homepage for the client is the Elastic guide page for the Rust API.

The first thing the README says after the badges is the caveat: the project is still very much a work in progress and in an alpha state, input and contributions welcome. That is the framing to hold on to, because everything else about the client is designed to a mature standard while the version numbers are not.

The release list makes that concrete. The most recent tags are v9.1.0-alpha.1 and v8.19.0-alpha.1, both from 2025-08-08, and v8.17.0-alpha.1 from December 2024. These are alpha builds of the major Elasticsearch lines rather than stable releases, and the naming convention is the interesting part: there is a client tag per Elasticsearch branch, and the alpha suffix applies to the client generation, not to the server.

The last push to the repository was on 2026-10-19 and the repository is not archived, so the work continues even though the tagged builds are alpha.

The licence is Apache-2.0, and the repository has the standard Elastic layout with a Cargo.toml workspace, an elasticsearch/ crate, an api_generator/ directory, an xtask/ directory, a Makefile.toml and a .buildkite/ directory for CI.

Generated from the REST specs, which is why the namespaced clients look the way they do

The client exposes all Elasticsearch APIs as associated functions, either on the root client, Elasticsearch, or on one of the namespaced clients such as Cat and Indices. The reason for that structure is given directly: the namespaced clients are based on the grouping of APIs within the Elasticsearch REST API specs, from which much of the client is generated.

That generation is the design decision that defines this library, and it is worth understanding what it buys and what it costs.

What it buys is coverage that does not rot. When Elasticsearch adds an endpoint or renames a parameter, the spec changes and the client regenerates. A hand-written client library accumulates a permanent backlog of endpoints that are slightly out of date, and the more it falls behind the more people write raw HTTP calls against the same API.

What it costs is that the client is not hand-tuned, and the shape of the API is dictated by the spec rather than by what would be most ergonomic in Rust. You can see that in the example call, where a search is issued through a builder with a parts enum:

rust
use elasticsearch::{Elasticsearch, Error, SearchParts};
use serde_json::{json, Value};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Elasticsearch::default();

    // make a search API call
    let search_response = client
        .search(SearchParts::None)
        .body(json!({
            "query": {
                "match_all": {}
            }
        }))
        .allow_no_indices(true)
        .send()
        .await?;

Every parameter is a method on a builder rather than a field, and the body is a serde_json value. The result is verbose but it is uniform, and uniformity across a hundred endpoints is what a generated client is for.

The api_generator/ directory and the xtask/ directory at the repository root are the machinery behind that, and the Makefile.toml is how the tasks are driven.

Async only, and the tokio dependency that follows from it

All API functions are async only, and can be awaited. There is no synchronous variant, which is a deliberate choice with consequences for how the crate is used.

The runtime underneath is reqwest, which internally uses the tokio runtime for async support. The documentation is explicit about the consequence: you may require to take a dependency on tokio in order to use the client. The Cargo.toml snippet is:

toml
tokio = { version = "*", features = ["full"] }

And your entry point has to be attributed with the tokio main attribute, with test functions attributed similarly:

rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // your code ...
    Ok(())
}

That is a real constraint on adoption. A synchronous codebase that wants to call Elasticsearch from a blocking context has to run an async runtime, since the README does not document a blocking API. The same applies to a test suite that is not already async, since the test attribute changes the shape of every test that touches the client.

It is also the right constraint for a modern Rust service. A client that only speaks async fits into an async server without blocking a thread, and reqwest's connection pooling is one of the reasons to prefer it over a hand-rolled HTTP client.

The compatibility model is stated in one sentence and is easy to get wrong if you skip it. The client is forward compatible, meaning it supports communicating with greater minor versions of Elasticsearch, and Elasticsearch language clients are also backwards compatible with lesser supported minor Elasticsearch versions. So the rule is to pick a client version matching the server version you run, and you will not be broken by the server moving ahead within a minor range.

Transports: default, single node, cloud ID, and the builder for everything else

The transport is where the connection to a cluster is configured, and there are four ways to build one.

The simplest is the default client, which talks to Elasticsearch on https://localhost:9200:

rust
use elasticsearch::Elasticsearch;

fn main() {
    let client = Elasticsearch::default();
}

For a specific URL, you build a single node transport:

rust
use elasticsearch::{
    Elasticsearch, Error,
    http::transport::Transport
};

fn main() -> Result<(), Error> {
    let transport = Transport::single_node("https://example.com")?;
    let client = Elasticsearch::new(transport);
    Ok(())
}

For Elastic Cloud, there is a cloud function that takes a Cloud ID and credentials. The credentials can be Basic, Bearer or ApiKey, so the example uses Basic with a username and password.

And for everything else, TransportBuilder is the general form, which is where proxies, authentication schemes and certificate validation are configured. The builder example shows a connection pool and a disabled proxy:

rust
use url::Url;
use elasticsearch::{
    Error, Elasticsearch,
    http::transport::{TransportBuilder,SingleNodeConnectionPool},
};

fn main() -> Result<(), Error> {
    let url = Url::parse("https://example.com")?;
    let conn_pool = SingleNodeConnectionPool::new(url);
    let transport = TransportBuilder::new(conn_pool).disable_proxy().build()?;
    let client = Elasticsearch::new(transport);
    Ok(())
}

The pooling model is the other half of it. MultiNodeConnectionPool configures the list of nodes of a cluster, and the client does round-robin load balancing among those nodes, and it can also periodically re-seed the list of nodes by querying the cluster. That re-seeding is what keeps a client working when the cluster topology changes under it.

Cargo features: TLS choice, and API tiers you opt into deliberately

The feature flags are the main compile-time decisions, and they fall into two groups.

The first is TLS. native-tls is enabled by default and enables TLS functionality provided by native-tls, while rustls-tls enables TLS functionality provided by rustls. That default is worth a moment's thought, because native-tls on most platforms means OpenSSL, which means a system library dependency and, in a container, either a base image with OpenSSL present or a build that links it statically. rustls avoids that, at the cost of not using the platform's certificate store by default unless you configure it.

The second group is API tiers, and the names describe the risk.

beta-apis enables beta APIs, which are on track to become stable and permanent features, and the warning is to use them with caution because it is possible that breaking changes are made to these APIs in a minor version. experimental-apis enables experimental APIs, which are just that, an experiment, and an experimental API might have breaking changes in any future version, or might even be removed entirely. That feature also enables beta-apis.

The severity difference between the two tiers is worth stating plainly, because the wording is easy to skim past. A beta API might change in a minor version, which is annoying but survivable. An experimental API can disappear entirely, at any version, with no deprecation cycle. For a service that has to keep running, that difference should decide whether a feature flag goes into your Cargo.toml at all.

One more capability is not a Cargo feature but worth knowing: the library runs in Web Assembly runtimes that provide the Fetch API, like node.js and web browsers. There is an examples/wasm_fetch/ directory in the repository, and the wasm_fetch example is the reference for that use.

Reading the response, and what the builder pattern costs you

The response handling has a detail that trips people up, and the example in the README is explicit about it.

After the send, you can get the HTTP response status code, and then you read the body, and the read consumes the response:

rust
    // get the HTTP response status code
    let status_code = search_response.status_code();

    // read the response body. Consumes search_response
    let response_body = search_response.json::<Value>().await?;

    // read fields from the response body
    let took = response_body["took"].as_i64().unwrap();

So status_code and json are mutually exclusive, and calling one means the other is gone. That is a normal Rust ownership outcome rather than a design flaw, but it means the shape of your error handling depends on whether you care about the status code or the body, not both. If you need to inspect a failed response body, for instance to read the Elasticsearch error reason, you have to structure the code so the body is read first and the status inspected afterwards, or use a lower-level path.

The json method is generic, so you can deserialize into serde_json::Value as the example does, or into your own struct with derive, and the optional serde and serde_json dependencies in the guide exist for exactly that.

The generated request bodies are not typed by default in this example; the body is a json! macro invocation. That is a trade the generated client makes, and it means you get full flexibility on the request side in exchange for no compile-time checking of the request body shape, while the response side is where you would want the types.

The response is where the interesting part of any client is, and the response is where typed deserialization pays off.

Editorial conclusion

Adopt elasticsearch-rs if you are writing a Rust service that talks to Elasticsearch and want typed request builders plus async support without hand-writing HTTP calls. Do not adopt it for a project that cannot tolerate an alpha client, because the README still describes the project as very much a work in progress and in an alpha state. Before you pick a version, match the crate version to the Elasticsearch version you run, since the client is forward compatible with greater minor versions and backwards compatible with lesser supported minors, and decide on your TLS feature early, since native-tls is enabled by default and switching to rustls is a Cargo.toml change rather than a runtime setting.

Frequently asked questions

How do I add the Elasticsearch Rust client to my project?

Add the elasticsearch crate to Cargo.toml, choosing the version compatible with the Elasticsearch version you are using, for example elasticsearch = "9.1.0-alpha.1". The optional serde and serde_json dependencies are useful for creating requests and reading responses.

Does the Rust client support synchronous calls?

No. All API functions are async only and can be awaited. The client uses reqwest, which uses the tokio runtime, so you may need a tokio dependency with the full feature, and to attribute your main function with #[tokio::main] and test functions with #[tokio::test].

How do I connect to an Elastic Cloud deployment?

Use Transport::cloud with a Cloud ID and credentials retrieved from the Cloud web console. Credentials can be Basic, Bearer or ApiKey, and the client is then created with Elasticsearch::new(transport).

What is the difference between the beta-apis and experimental-apis Cargo features?

beta-apis enables APIs on track to become stable, where breaking changes are possible in a minor version. experimental-apis enables APIs that might have breaking changes in any future version, or might be removed entirely, and it also enables beta-apis.

Official sources

  1. elastic/elasticsearch-rs on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/elastic-elasticsearch-rs.svg)](https://hysenlabs.com/projects/elastic-elasticsearch-rs)

Was this useful?