Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

The Google Cloud Client Libraries for Rust is a collection of Rust crates to interact with Google Cloud Services.

This guide is organized as a series of small tutorials showing how to perform specific actions with the client libraries. Most Google Cloud services follow a series of guidelines, collectively known as the AIPs. This makes the client libraries more consistent from one service to the next. The functions to delete or list resources almost always have the same interface.

Audience

This guide is intended for Rust developers who are familiar with the language and the Rust ecosystem. We will assume you know how to use Rust and its supporting toolchain.

At the risk of being repetitive, most of the guides do not assume you have used any Google Service or client library before (in Rust or other language). However, the guides will refer you to service specific tutorials to initialize their projects and services.

Service specific documentation

These guides are not intended as tutorials for each service or as extended guides on how to design Rust applications to work on Google Cloud. They are starting points to get you productive with the client libraries for Rust.

We recommend you read the service documentation at https://cloud.google.com to learn more about each service. If you need guidance on how to design your application for Google Cloud, the Cloud Architecture Center may have what you are looking for.

Reporting bugs

We welcome bugs about the client libraries or their documentation. Please use GitHub Issues.

License

The client libraries source and their documentation are release under the Apache License, Version 2.0.

Setting up your development environment

Prepare your environment for Rust app development and deployment on Google Cloud by installing the following tools.

Install Rust

  1. To install Rust, see Getting Started.

  2. Confirm that you have the most recent version of Rust installed:

    cargo --version
    

Install an editor

The Getting Started guide links popular editor plugins and IDEs, which provide the following features:

  • Fully integrated debugging capabilities
  • Syntax highlighting
  • Code completion

Install the Google Cloud CLI

The Google Cloud CLI is a set of tools for Google Cloud. It contains the gcloud and bq command-line tools used to access Compute Engine, Cloud Storage, BigQuery, and other services from the command line. You can run these tools interactively or in your automated scripts.

To install the gcloud CLI, see Installing the gcloud CLI.

Install the Cloud Client Libraries for Rust in a new project

The Cloud Client Libraries for Rust is the idiomatic way for Rust developers to integrate with Google Cloud services, such as Secret Manager and Workflows.

For example, to use the package for an individual API, such as the Secret Manager API, do the following:

  1. Create a new Rust project:

    cargo new my-project
    
  2. Change your directory to the new project:

    cd my-project
    
  3. Add the Secret Manager client library to the new project:

    cargo add google-cloud-secretmanager-v1
    

    If you haven't already enabled the Secret Manager API, enable it in APIs and services or by running the following command:

    gcloud services enable secretmanager.googleapis.com
    
  4. Add the google-cloud-gax crate to the new project:

    cargo add google-cloud-gax
    
  5. Add the tokio crate to the new project:

    cargo add tokio --features macros
    
  6. Edit src/main.rs in your project to use the Secret Manager client library:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use google_cloud_gax::paginator::ItemPaginator as _;
    use google_cloud_secretmanager_v1::client::SecretManagerService;
    let project_id = std::env::args().nth(1).unwrap();
    let client = SecretManagerService::builder().build().await?;

    let mut items = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(item) = items.next().await {
        println!("{}", item?.name);
    }
    Ok(())
}
  1. Build your program:

    cargo build
    

    The program should build without errors.

Note: The source of the Cloud Client Libraries for Rust is on GitHub.

Running the program

  1. To use the Cloud Client Libraries in a local development environment, set up Application Default Credentials.

    gcloud auth application-default login
    

    For more information, see Authenticate for using client libraries.

  2. Run your program, supplying your Google Cloud Platform project's ID:

    PROJECT_ID=$(gcloud config get project)
    cargo run ${PROJECT_ID}
    

    The program will print the secrets associated with your project ID. If you don't see any secrets, you might not have any in Secret Manager. You can create a secret and rerun the program, and you should see the secret printed in the output.

What's next

Setting up Rust on Cloud Shell

Cloud Shell is a great environment to run small examples and tests. This guide shows you how to configure Rust and install one of the Cloud Client Libraries in Cloud Shell.

Start up Cloud Shell

  1. In the Google Cloud console project selector, select a project.

  2. Open https://shell.cloud.google.com to start a new shell. You might be prompted to authorize Cloud Shell to use your credentials for Google Cloud API calls.

Configure Rust

  1. Cloud Shell comes with rustup pre-installed. You can use it to install and configure the default version of Rust:

    rustup default stable
    
  2. Confirm that you have the most recent version of Rust installed:

    cargo --version
    

Install Rust client libraries in Cloud Shell

  1. Create a new Rust project:

    cargo new my-project
    
  2. Change your directory to the new project:

    cd my-project
    
  3. Add the Secret Manager client library to the new project:

    cargo add google-cloud-secretmanager-v1
    
  4. Add the google-cloud-gax crate to the new project:

    cargo add google-cloud-gax
    
  5. Add the tokio crate to the new project:

    cargo add tokio --features macros
    
  6. Edit src/main.rs in your project to use the Secret Manager client library:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    use google_cloud_gax::paginator::ItemPaginator as _;
    use google_cloud_secretmanager_v1::client::SecretManagerService;
    let project_id = std::env::args().nth(1).unwrap();
    let client = SecretManagerService::builder().build().await?;

    let mut items = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(item) = items.next().await {
        println!("{}", item?.name);
    }
    Ok(())
}
  1. Run your program, supplying your Google Cloud Platform project's ID:

    PROJECT_ID=$(gcloud config get project)
    cargo run ${PROJECT_ID}
    

    The program will print the secrets associated with your project ID. If you don't see any secrets, you might not have any in Secret Manager. You can create a secret and rerun the program, and you should see the secret printed in the output.

How to initialize a client

The Google Cloud Client Libraries for Rust use clients as the main abstraction to interface with specific services. Clients are implemented as Rust structs, with methods corresponding to each RPC offered by the service. To use a Google Cloud service using the Rust client libraries, you need to first initialize a client.

Prerequisites

In this guide you'll initialize a client and then use the client to make a simple RPC. For this tutorial, you'll use the Secret Manager API. The same structure applies to any other service in Google Cloud.

We recommend that you follow one of the Secret Manager getting started guides, such as how to Create a secret, before attempting to use the client library. These guides cover service specific concepts in more detail, and provide detailed guidance on project prerequisites.

We also recommend that you follow the instructions in the Authenticate for using client libraries guide. This guide will show you how to log in to configure the Application Default Credentials used in this guide.

Dependencies

As usual with Rust, you must declare the dependency in your Cargo.toml file:

cargo add google-cloud-secretmanager-v1

To initialize a client, you first call Client::builder() to obtain an appropriate ClientBuilder and then call build() on that builder to create a client.

The following creates a client with the default configuration, which is designed to meet requirements for most use cases.

    let client = SecretManagerService::builder().build().await?;

Once the client is successfully initialized, you can use it to make RPCs:

    use google_cloud_gax::paginator::Paginator as _;
    let mut items = client
        .list_locations()
        .set_name(format!("projects/{project_id}"))
        .by_page();
    while let Some(page) = items.next().await {
        let page = page?;
        for location in page.locations {
            println!("{}", location.name);
        }
    }

This example shows a call to list_locations, which returns information about the supported locations for the service (in this case, Secret Manager). The output of the example should look something like this:

projects/123456789012/locations/europe-west8
projects/123456789012/locations/europe-west9
projects/123456789012/locations/us-east5
...

Full program

Putting all this code together into a full program looks as follows:

pub type Result = std::result::Result<(), Box<dyn std::error::Error>>;

pub async fn initialize_client(project_id: &str) -> Result {
    use google_cloud_secretmanager_v1::client::SecretManagerService;

    // Initialize a client with the default configuration. This is an
    // asynchronous operation that may fail, as it requires acquiring an an
    // access token.
    let client = SecretManagerService::builder().build().await?;

    // Once initialized, use the client to make requests.
    use google_cloud_gax::paginator::Paginator as _;
    let mut items = client
        .list_locations()
        .set_name(format!("projects/{project_id}"))
        .by_page();
    while let Some(page) = items.next().await {
        let page = page?;
        for location in page.locations {
            println!("{}", location.name);
        }
    }

    Ok(())
}

What's next

This guide showed you how to initialize a client using the Google Cloud Client Libraries for Rust. For a more complex example of working with a service, check out Generate text using the Vertex AI Gemini API.

Generate text using the Vertex AI Gemini API

In this guide, you send a text prompt request, and then a multimodal prompt and image request to the Vertex AI Gemini API and view the responses.

Prerequisites

To complete this guide, you must have a Google Cloud project with the Vertex AI API enabled. You can use the Vertex AI setup guide to complete these steps.

Add the Vertex AI client library as a dependency

The Vertex AI client library includes many features. Compiling all of them is relatively slow. To speed up compilation, you can just enable the features you need:

cargo add google-cloud-aiplatform-v1 --no-default-features --features prediction-service

Send a prompt to the Vertex AI Gemini API

First, initialize the client using the default settings:

    use google_cloud_aiplatform_v1 as vertexai;
    let client = vertexai::client::PredictionService::builder()
        .build()
        .await?;

Then build the model name. For simplicity, this example receives the project ID as an argument and uses a fixed location (global) and model id (gemini-2.0-flash-001).

    const MODEL: &str = "gemini-2.0-flash-001";
    let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");

If you want to run this function in your own code, use the project id (without any projects/ prefix) of the project you selected while going through the prerequisites.

With the client initialized you can send the request:

    let response = client
        .generate_content().set_model(&model)
        .set_contents([vertexai::model::Content::new().set_role("user").set_parts([
            vertexai::model::Part::new().set_text("What's a good name for a flower shop that specializes in selling bouquets of dried flowers?"),
        ])])
        .send()
        .await;

And then print the response. You can use the :#? format specifier to prettify the nested response objects:

    println!("RESPONSE = {response:#?}");

See below for the complete code.

Send a prompt and an image to the Vertex AI Gemini API

As in the previous example, initialize the client using the default settings:

    use google_cloud_aiplatform_v1 as vertexai;
    let client = vertexai::client::PredictionService::builder()
        .build()
        .await?;

And then build the model name:

    const MODEL: &str = "gemini-2.0-flash-001";
    let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");

The new request includes an image part:

                vertexai::model::Part::new().set_file_data(
                    vertexai::model::FileData::new()
                        .set_mime_type("image/jpeg")
                        .set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
                ),

And the prompt part:

                vertexai::model::Part::new().set_text("Describe this picture."),

Send the full request:

    let response = client
        .generate_content()
        .set_model(&model)
        .set_contents(
            [vertexai::model::Content::new().set_role("user").set_parts([
                vertexai::model::Part::new().set_file_data(
                    vertexai::model::FileData::new()
                        .set_mime_type("image/jpeg")
                        .set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
                ),
                vertexai::model::Part::new().set_text("Describe this picture."),
            ])],
        )
        .send()
        .await;

As in the previous example, print the full response:

    println!("RESPONSE = {response:#?}");

See below for the complete code.


Text prompt: complete code

pub async fn text_prompt(project_id: &str) -> crate::Result<()> {
    use google_cloud_aiplatform_v1 as vertexai;
    let client = vertexai::client::PredictionService::builder()
        .build()
        .await?;

    const MODEL: &str = "gemini-2.0-flash-001";
    let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");

    let response = client
        .generate_content().set_model(&model)
        .set_contents([vertexai::model::Content::new().set_role("user").set_parts([
            vertexai::model::Part::new().set_text("What's a good name for a flower shop that specializes in selling bouquets of dried flowers?"),
        ])])
        .send()
        .await;
    println!("RESPONSE = {response:#?}");

    Ok(())
}

Prompt and image: complete code

pub async fn prompt_and_image(project_id: &str) -> crate::Result<()> {
    use google_cloud_aiplatform_v1 as vertexai;
    let client = vertexai::client::PredictionService::builder()
        .build()
        .await?;

    const MODEL: &str = "gemini-2.0-flash-001";
    let model = format!("projects/{project_id}/locations/global/publishers/google/models/{MODEL}");

    let response = client
        .generate_content()
        .set_model(&model)
        .set_contents(
            [vertexai::model::Content::new().set_role("user").set_parts([
                vertexai::model::Part::new().set_file_data(
                    vertexai::model::FileData::new()
                        .set_mime_type("image/jpeg")
                        .set_file_uri("gs://generativeai-downloads/images/scones.jpg"),
                ),
                vertexai::model::Part::new().set_text("Describe this picture."),
            ])],
        )
        .send()
        .await;
    println!("RESPONSE = {response:#?}");

    Ok(())
}

Using Google Cloud Storage

Google Cloud Storage is a managed service for storing unstructured data.

The Rust client library provides an idiomatic API to access this service. The client library resumes interrupted downloads and uploads, and automatically performs integrity checks on the data. For metadata operations, the client library can retry failed requests, and automatically poll long-running operations.

Quickstart

This guide will show you how to create a Cloud Storage bucket, upload an object to this bucket, and then read the object back.

Prerequisites

The guide assumes you have an existing Google Cloud project with billing enabled.

Add the client library as a dependency

cargo add google-cloud-storage

Create a storage bucket

The client to perform operations on buckets and object metadata is called StorageControl:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

To create a bucket you must provide the project name and the desired bucket id:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

You can also provide other attributes for the bucket. For example, if you want all objects in the bucket to use the same permissions, you can enable Uniform bucket-level access:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Then send this request and wait for the response:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Upload an object

The client to perform operations on object data is called Storage:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

In this case we will create an object called hello.txt, with the traditional greeting for a programming tutorial:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Download an object

To download the contents of an object use read_object():

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Cleanup

Finally we remove the object and bucket to cleanup all the resources used in this guide:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Next Steps

Full program

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub async fn quickstart(project_id: &str, bucket_id: &str) -> anyhow::Result<()> {
    use google_cloud_storage as gcs;
    use google_cloud_storage::client::StorageControl;
    let control = StorageControl::builder().build().await?;
    let bucket = control
        .create_bucket()
        .set_parent("projects/_")
        .set_bucket_id(bucket_id)
        .set_bucket(
            gcs::model::Bucket::new()
                .set_project(format!("projects/{project_id}"))
                .set_iam_config(
                    gcs::model::bucket::IamConfig::new().set_uniform_bucket_level_access(
                        gcs::model::bucket::iam_config::UniformBucketLevelAccess::new()
                            .set_enabled(true),
                    ),
                ),
        )
        .send()
        .await?;
    println!("bucket successfully created {bucket:?}");

    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let object = client
        .write_object(&bucket.name, "hello.txt", "Hello World!")
        .send_buffered()
        .await?;
    println!("object successfully uploaded {object:?}");

    let mut reader = client.read_object(&bucket.name, "hello.txt").send().await?;
    let mut contents = Vec::new();
    while let Some(chunk) = reader.next().await.transpose()? {
        contents.extend_from_slice(&chunk);
    }
    println!(
        "object contents successfully downloaded {:?}",
        bytes::Bytes::from_owner(contents)
    );

    control
        .delete_object()
        .set_bucket(&bucket.name)
        .set_object(&object.name)
        .set_generation(object.generation)
        .send()
        .await?;
    control
        .delete_bucket()
        .set_name(&bucket.name)
        .send()
        .await?;

    Ok(())
}

Push data on object writes

The client API to write Cloud Storage objects pulls the payload from a type provided by the application. Some applications generate the payload in a thread and would rather "push" the object payload to the service.

This guide shows you how to write an object to Cloud Storage using a push data source.

Prerequisites

The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.

Add the client library as a dependency

cargo add google-cloud-storage

Convert a queue to a StreamingSource

The key idea is to use a queue to separate the task pushing new data from the task pulling the payload. This tutorial uses a Tokio mpsc queue, but you can use any queue that integrates with Tokio's async runtime.

First wrap the receiver in our own type:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Then implement the trait required by the Google Cloud client library:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

In this tutorial you write the rest of the code in a function that accepts the bucket and object name as parameters:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}
    // ... code goes here ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Initialize a client:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Create a queue, obtaining the receiver and sender:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Use the client to write an object with the data received from this queue. Note that we do not await the future created in the write_object() method.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Create a task to process the queue and write the data in the background:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

In the main task, send some data to write:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Once you have finished sending the data, drop the sender to close the sending side of the queue:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Now you can wait for the task to finish and extract the result:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Full program

Putting all these steps together you get:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_storage::streaming_source::StreamingSource;
use tokio::sync::mpsc::{self, Receiver};
#[derive(Debug)]
struct QueueSource(Receiver<bytes::Bytes>);
impl StreamingSource for QueueSource {
    type Error = std::convert::Infallible;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0.recv().await.map(Ok)
    }
}

pub async fn queue(bucket_name: &str, object_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;

    let (sender, receiver) = mpsc::channel::<bytes::Bytes>(32);
    let upload = client
        .write_object(bucket_name, object_name, QueueSource(receiver))
        .send_buffered();
    let task = tokio::spawn(upload);

    for _ in 0..1000 {
        let line = "I will not write funny examples in class\n";
        sender
            .send(bytes::Bytes::from_static(line.as_bytes()))
            .await?;
    }
    drop(sender);
    let object = task.await??;
    println!("object successfully uploaded {object:?}");

    Ok(())
}

Rewriting objects

Rewriting a Cloud Storage object can require multiple client requests, depending on the details of the operation. In such cases, the service will return a response representing its progress, along with a rewrite_token which the client must use to continue the operation.

This guide will show you how to fully execute the rewrite loop for a Cloud Storage object.

Prerequisites

The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.

Add the client library as a dependency

cargo add google-cloud-storage

Rewriting an object

Prepare client

First, create a client.

The service recommends an overall timeout of at least 30 seconds. In this example, we use a RetryPolicy that does not set any timeout on the operation.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Prepare builder

Next we prepare the request builder.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Optionally, we can limit the maximum amount of bytes written per call, before the service responds with a progress report. Setting this option is an alternative to increasing the attempt timeout.

Note that the value used in this example is intentionally small to force the rewrite loop to take multiple iterations. In practice, you would likely use a larger value.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Rewriting an object allows you to copy its data to a different bucket, copy its data to a different object in the same bucket, change its encryption key, and/or change its storage class. The rewrite loop is identical for all these transformations. We will change the storage class to illustrate the code.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Note that there is a minimum storage duration associated with the new storage class. While the object used in this example (3 MiB) incurs less than $0.001 of cost, the billing may be noticeable for larger objects.

Introduce rewrite loop helpers

Next, we introduce a helper function to perform one iteration of the rewrite loop.

We send the request and process the response. We log the progress made.

If the operation is done, we return the object metadata, otherwise we return the rewrite token.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Execute rewrite loop

Now we are ready to perform the rewrite loop until the operation is done.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Note how if the operation is incomplete, we supply the rewrite token returned by the server to the next request.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

Also note that the rewrite token can be used to continue the operation from another process. Rewrite tokens are valid for up to one week.

Full program

Putting all these steps together you get:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use gcs::Result;
use gcs::builder::storage_control::RewriteObject;
use gcs::client::StorageControl;
use gcs::model::Object;
use gcs::retry_policy::RetryableErrors;
use google_cloud_gax::retry_policy::RetryPolicyExt as _;
use google_cloud_storage as gcs;

pub async fn rewrite_object(bucket_name: &str) -> anyhow::Result<()> {
    let source_object = upload(bucket_name).await?;

    let control = StorageControl::builder()
        .with_retry_policy(RetryableErrors.with_attempt_limit(5))
        .build()
        .await?;

    let mut builder = control
        .rewrite_object()
        .set_source_bucket(bucket_name)
        .set_source_object(&source_object.name)
        .set_destination_bucket(bucket_name)
        .set_destination_name("rewrite-object-clone");

    // Optionally limit the max bytes written per request.
    builder = builder.set_max_bytes_rewritten_per_call(1024 * 1024);

    // Optionally change the storage class to force GCS to copy bytes
    builder = builder.set_destination(Object::new().set_storage_class("NEARLINE"));

    let dest_object = loop {
        let progress = make_one_request(builder.clone()).await?;
        match progress {
            RewriteProgress::Incomplete(rewrite_token) => {
                builder = builder.set_rewrite_token(rewrite_token);
            }
            RewriteProgress::Done(object) => break object,
        };
    };
    println!("dest_object={dest_object:?}");

    cleanup(control, bucket_name, &source_object.name, &dest_object.name).await;
    Ok(())
}

enum RewriteProgress {
    // This holds the rewrite token
    Incomplete(String),
    Done(Box<Object>),
}

async fn make_one_request(builder: RewriteObject) -> Result<RewriteProgress> {
    let resp = builder.send().await?;
    if resp.done {
        println!(
            "DONE:     total_bytes_rewritten={}; object_size={}",
            resp.total_bytes_rewritten, resp.object_size
        );
        return Ok(RewriteProgress::Done(Box::new(
            resp.resource
                .expect("A `done` response must have an object."),
        )));
    }
    println!(
        "PROGRESS: total_bytes_rewritten={}; object_size={}",
        resp.total_bytes_rewritten, resp.object_size
    );
    Ok(RewriteProgress::Incomplete(resp.rewrite_token))
}

// Upload an object to rewrite
async fn upload(bucket_name: &str) -> anyhow::Result<Object> {
    let storage = gcs::client::Storage::builder().build().await?;
    // We need the size to exceed 1MiB to exercise the rewrite token logic.
    let payload = bytes::Bytes::from(vec![65_u8; 3 * 1024 * 1024]);
    let object = storage
        .write_object(bucket_name, "rewrite-object-source", payload)
        .send_unbuffered()
        .await?;
    Ok(object)
}

// Clean up the resources created in this sample
async fn cleanup(control: StorageControl, bucket_name: &str, o1: &str, o2: &str) {
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o1)
        .send()
        .await;
    let _ = control
        .delete_object()
        .set_bucket(bucket_name)
        .set_object(o2)
        .send()
        .await;
    let _ = control.delete_bucket().set_name(bucket_name).send().await;
}

We should see output similar to:

PROGRESS: total_bytes_rewritten=1048576; object_size=3145728
PROGRESS: total_bytes_rewritten=2097152; object_size=3145728
DONE:     total_bytes_rewritten=3145728; object_size=3145728
dest_object=Object { name: "rewrite-object-clone", ... }

Speed up large object downloads

In this tutorial you will learn how to use striped downloads to speed up downloads of large Cloud Storage objects.

Prerequisites

The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.

You will create some large objects during this tutorial, remember to clean up any resources to avoid excessive billing.

The tutorial assumes you are familiar with the basics of using the client library. If not, read the quickstart guide.

Add the client library as a dependency

cargo add google-cloud-storage

Create source data

To run this tutorial you will need some large objects in Cloud Storage. You can create such objects by seeding a smaller object and then repeatedly composing it to create objects of the desired size.

You can put all the code for seeding the data in its own function. This function will receive the storage and storage control clients as parameters. For information on how to create these clients, consult the quickstart guide:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}
    // ... details omitted ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

As usual, the function starts with some use declarations to simplify the code:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Using the storage client, you create a 1MiB object:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Then you use the storage control client to concatenate 32 copies of this object into a larger object. This operation does not require transferring any object data to the client, it is performed by the service:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

You can repeat the operation to create larger and larger objects:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Striped downloads

Again, write a function to perform the striped download:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}
    // ... details below ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Use the storage control client to query the object metadata. This metadata includes the object size and the current generation:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

We will split the download of each stripe to a separate function. You will see the details of this function in a moment, for now just note that it is async, so it returns a Future:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}
    // ... details below ...
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

You can compute the size of each stripe and then call write_stripe() to download each of these stripes. Then collect the results into a vector:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

You can use the standard Rust facilities to concurrently await all these futures:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Once they complete, the file is downloaded.

Now you should complete writing the write_stripe() function. First, duplicate the write object and prepare to write starting at the desired offset:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Start a download from Cloud Storage:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

To restrict the download to the desired stripe, use .with_read_offset() and .with_read_limit():

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

You may also want to restrict the download to the right object generation. This will avoid race conditions where another process writes over the object and you get inconsistent reads:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Then you read the data and write it to the local file:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Next steps

  • Consider optimizing the case where the last stripe only has a few bytes

Expected performance

The performance of these downloads depends on:

  • The I/O subsystem: if your local storage is not fast enough the downloads will be throttled by the writes to disk.
  • The configuration of your VM: if you do not have enough CPUs, the downloads will be throttled on trying to decrypt the on data, as Cloud Storage and the client library always encrypt the data in transit.
  • The location of the bucket and the particular object: the bucket may store all of the objects (or some objects) in a region different from your VM's location. In this case, you may be throttled by the wide-area network capacity.

With a large enough VM, using SSD for disk, and with a bucket in the same region as the VM you should get close to 1,000 MiB/s of effective throughput.

Full program

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.


use google_cloud_storage::client::Storage;
use google_cloud_storage::client::StorageControl;
use google_cloud_storage::model::Object;

async fn seed(client: Storage, control: StorageControl, bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::model::compose_object_request::SourceObject;

    let buffer = String::from_iter(('a'..='z').cycle().take(1024 * 1024));
    let seed = client
        .write_object(bucket_name, "1MiB.txt", bytes::Bytes::from_owner(buffer))
        .send_unbuffered()
        .await?;
    println!(
        "Uploaded object {}, size={}KiB",
        seed.name,
        seed.size / 1024
    );

    let seed_32 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("32MiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed.name)
                .set_generation(seed.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    let seed_1024 = control
        .compose_object()
        .set_destination(Object::new().set_bucket(bucket_name).set_name("1GiB.txt"))
        .set_source_objects((0..32).map(|_| {
            SourceObject::new()
                .set_name(&seed_32.name)
                .set_generation(seed_32.generation)
        }))
        .send()
        .await?;
    println!(
        "Created object {}, size={}MiB",
        seed.name,
        seed.size / (1024 * 1024)
    );

    for s in [2, 4, 8, 16, 32] {
        let name = format!("{s}GiB.txt");
        let target = control
            .compose_object()
            .set_destination(Object::new().set_bucket(bucket_name).set_name(&name))
            .set_source_objects((0..s).map(|_| {
                SourceObject::new()
                    .set_name(&seed_1024.name)
                    .set_generation(seed_1024.generation)
            }))
            .send()
            .await?;
        println!(
            "Created object {} size={} MiB",
            target.name,
            target.size / (1024 * 1024)
        );
    }

    Ok(())
}

async fn download(
    client: Storage,
    control: StorageControl,
    bucket_name: &str,
    object_name: &str,
    stripe_size: usize,
    destination: &str,
) -> anyhow::Result<()> {
    let metadata = control
        .get_object()
        .set_bucket(bucket_name)
        .set_object(object_name)
        .send()
        .await?;

    let file = tokio::fs::File::create(destination).await?;
    let start = std::time::Instant::now();

    let size = metadata.size as u64;
    let limit = stripe_size as u64;
    let count = size / limit;
    let mut stripes = (0..count)
        .map(|i| write_stripe(client.clone(), &file, i * limit, limit, &metadata))
        .collect::<Vec<_>>();
    if size % limit != 0 {
        stripes.push(write_stripe(
            client.clone(),
            &file,
            count * limit,
            limit,
            &metadata,
        ))
    }

    futures::future::join_all(stripes)
        .await
        .into_iter()
        .collect::<anyhow::Result<Vec<_>>>()?;

    let elapsed = std::time::Instant::now() - start;
    let mib = metadata.size as f64 / (1024.0 * 1024.0);
    let bw = mib / elapsed.as_secs_f64();
    println!(
        "Completed {mib:.2} MiB download in {elapsed:?}, using {count} stripes, effective bandwidth = {bw:.2} MiB/s"
    );

    Ok(())
}

async fn write_stripe(
    client: Storage,
    file: &tokio::fs::File,
    offset: u64,
    limit: u64,
    metadata: &Object,
) -> anyhow::Result<()> {
    use google_cloud_storage::model_ext::ReadRange;
    use tokio::io::AsyncSeekExt;
    let mut writer = file.try_clone().await?;
    writer.seek(std::io::SeekFrom::Start(offset)).await?;
    let mut reader = client
        .read_object(&metadata.bucket, &metadata.name)
        .set_generation(metadata.generation)
        .set_read_range(ReadRange::segment(offset, limit))
        .send()
        .await?;
    while let Some(b) = reader.next().await.transpose()? {
        use tokio::io::AsyncWriteExt;
        writer.write_all(&b).await?;
    }
    Ok(())
}

pub async fn test(bucket_name: &str, destination: &str) -> anyhow::Result<()> {
    const MB: usize = 1024 * 1024;
    let client = Storage::builder().build().await?;
    let control = StorageControl::builder().build().await?;
    seed(client.clone(), control.clone(), bucket_name).await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        32 * MB,
        destination,
    )
    .await?;
    download(
        client.clone(),
        control.clone(),
        bucket_name,
        "32MiB.txt",
        MB,
        destination,
    )
    .await?;
    #[cfg(feature = "run-large-downloads")]
    {
        let destination = "/dev/shm/output";
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "32GiB.txt",
            256 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            1024 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "1GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "4GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "8GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
        download(
            client.clone(),
            control.clone(),
            bucket_name,
            "16GiB.txt",
            512 * MB,
            destination,
        )
        .await?;
    }
    Ok(())
}

Use errors to terminate object writes

In this guide you will learn how to use errors and custom data sources to terminate an object write before it is finalized. This is useful when applications want to stop the client library from finalizing the object creation if there is some error condition.

Prerequisites

The guide assumes you have an existing Google Cloud project with billing enabled, and a Cloud Storage bucket in that project.

The tutorial assumes you are familiar with the basics of using the client library. If not, you may want to read the quickstart guide first.

Add the client library as a dependency

cargo add google-cloud-storage

Overview

The client library creates objects from any type implementing the StreamingSource trait. The client library pulls data from implementations of the trait. The library terminates the object write on the first error.

In this guide you will build a custom implementation of StreamingSource that returns some data and then stops on an error. You will verify that an object write using this custom data source returns an error.

Create a custom error type

To terminate an object write without finalizing it, your StreamingSource must return an error. In this example you will create a simple error type, in your application code you can use any existing error type:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

The client library requires that your custom error type implements the standard Error trait:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

As you may recall, that requires implementing Display too:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Create a custom StreamingSource

Create a type that generates the data for your object. In this example you will use synthetic data using a counter:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Implement the streaming source trait for your type:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}
    // ... more details below ...
}

Define the error type:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

And implement the main function in this trait. Note how this function will (eventually) return the error type you defined above:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Create the object

You will need a client to interact with Cloud Storage:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Use the custom type to create the object:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

As expected, this object write fails. You can inspect the error details:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Next Steps

Full Program

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[derive(Debug)]
pub enum MyError {
    ExpectedProblem,
    OhNoes,
}

impl std::error::Error for MyError {}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::ExpectedProblem => write!(f, "this kind of thing happens"),
            Self::OhNoes => write!(f, "oh noes! something terrible happened"),
        }
    }
}

#[derive(Debug, Default)]
struct MySource(u32);

impl google_cloud_storage::streaming_source::StreamingSource for MySource {
    type Error = MyError;
    async fn next(&mut self) -> Option<Result<bytes::Bytes, Self::Error>> {
        self.0 += 1;
        match self.0 {
            42 => Some(Err(MyError::ExpectedProblem)),
            n if n > 42 => None,
            n => Some(Ok(bytes::Bytes::from_owner(format!(
                "test data for the example {n}\n"
            )))),
        }
    }
}

pub async fn attempt_upload(bucket_name: &str) -> anyhow::Result<()> {
    use google_cloud_storage::client::Storage;
    let client = Storage::builder().build().await?;
    let upload = client
        .write_object(bucket_name, "expect-error", MySource::default())
        .send_buffered()
        .await;
    println!("Upload result {upload:?}");
    let err = upload.expect_err("the source is supposed to terminate the upload");
    assert!(err.is_serialization(), "{err:?}");
    use std::error::Error as _;
    assert!(err.source().is_some_and(|e| e.is::<MyError>()), "{err:?}");
    Ok(())
}

Update a resource using a field mask

This guide shows you how to update a resource using a field mask, so that you can control which fields on the resource will be updated. The guide uses a secret from Secret Manager as a resource, but the concepts apply to other resources and services as well.

Prerequisites

To complete this tutorial, you need a Rust development environment with the following dependencies installed:

  • The Secret Manager client library
  • Tokio

To get set up, follow the steps in Setting up your development environment.

Install well known types

The google_cloud_wkt crate contains well known types for Google Cloud APIs. These types typically have custom JSON encoding, and may provide conversion functions to and from native or commonly used Rust types. google_cloud_wkt contains the field mask type, FieldMask, so you'll need to add the crate as a dependency:

cargo add google-cloud-wkt

FieldMask

A FieldMask represents a set of symbolic field paths. Field masks are used to specify a subset of fields that should be returned by a get operation or modified by an update operation.

A field mask in an update operation specifies which fields of the targeted resource should be updated. The API is required to change only the values of the fields specified in the mask and leave the others untouched. If a resource is passed in to describe the updated values, the API ignores the values of all fields not covered by the mask. If a field mask is not present on update, the operation applies to all fields (as if a field mask of all fields had been specified).

In order to reset a field to the default value, you must include the field in the mask and set the default value in the provided resource. Thus, in order to reset all fields of a resource, provide a default instance of the resource and set all fields in the mask, or don't provide a mask.

Update fields on a resource

First, initialize a Secret Manager client and create a secret:

    let client = SecretManagerService::builder().build().await?;

    let secret = client
        .create_secret()
        .set_parent(format!("projects/{project_id}"))
        .set_secret_id("your-secret")
        .set_secret(model::Secret::new().set_replication(
            model::Replication::new().set_automatic(model::replication::Automatic::new()),
        ))
        .send()
        .await?;
    println!("CREATE = {secret:?}");

If you examine the output from the create operation, you'll see that both the labels and annotations fields are empty.

The following code updates the labels and annotations fields:

    let tag = |mut labels: HashMap<_, _>, msg: &str| {
        labels.insert("updated".to_string(), msg.to_string());
        labels
    };

    let update = client
        .update_secret()
        .set_secret(
            model::Secret::new()
                .set_name(&secret.name)
                .set_etag(secret.etag)
                .set_labels(tag(secret.labels, "your-label"))
                .set_annotations(tag(secret.annotations, "your-annotations")),
        )
        .set_update_mask(
            google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
        )
        .send()
        .await?;
    println!("UPDATE = {update:?}");

The set_etag method lets you set an etag on the secret, which prevents overwriting concurrent updates.

Having set labels and annotations on the updated secret, you pass a field mask to set_update_mask specifying the field paths to be updated:

        .set_update_mask(
            google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
        )

In the output from the update operation, you can see that the fields have been updated:

labels: {"updated": "your-label"},
...
annotations: {"updated": "your-annotations"},

See below for the complete code.

What's next

In this guide, you updated a resource using a field mask. The sample code uses the Secret Manager API, but you can use field masks with other clients too. Try one of the other Cloud Client Libraries for Rust:


Update field: complete code

pub async fn update_field(project_id: &str) -> anyhow::Result<()> {
    use google_cloud_secretmanager_v1::client::SecretManagerService;
    use std::collections::HashMap;

    let client = SecretManagerService::builder().build().await?;

    let secret = client
        .create_secret()
        .set_parent(format!("projects/{project_id}"))
        .set_secret_id("your-secret")
        .set_secret(model::Secret::new().set_replication(
            model::Replication::new().set_automatic(model::replication::Automatic::new()),
        ))
        .send()
        .await?;
    println!("CREATE = {secret:?}");

    let tag = |mut labels: HashMap<_, _>, msg: &str| {
        labels.insert("updated".to_string(), msg.to_string());
        labels
    };

    let update = client
        .update_secret()
        .set_secret(
            model::Secret::new()
                .set_name(&secret.name)
                .set_etag(secret.etag)
                .set_labels(tag(secret.labels, "your-label"))
                .set_annotations(tag(secret.annotations, "your-annotations")),
        )
        .set_update_mask(
            google_cloud_wkt::FieldMask::default().set_paths(["annotations", "labels"]),
        )
        .send()
        .await?;
    println!("UPDATE = {update:?}");

    Ok(())
}

Configuring Retry Policies

The Google Cloud client libraries for Rust can automatically retry operations that fail due to transient errors.

This guide shows you how to customize the retry loop. First you'll learn how to enable a common retry policy for all requests in a client, and then how to override this default for a specific request.

Prerequisites

The guide uses the Secret Manager service. That makes the examples more concrete and therefore easier to follow. With that said, the same ideas work for any other service.

You may want to follow the service quickstart. This guide will walk you through the steps necessary to enable the service, ensure you have logged in, and that your account has the necessary permissions.

Dependencies

As usual with Rust, you must declare dependencies in your Cargo.toml file:

cargo add google-cloud-secretmanager-v1

Configuring the default retry policy

This example uses the Aip194Strict policy. This policy is based on the guidelines in AIP-194, which documents the conditions under which a Google API client should automatically retry a request. The policy is fairly conservative, and will not retry any error that indicates the request may have reached the service, unless the request is idempotent. As such, the policy is safe to use as a default. The only downside may be additional requests to the service, consuming some quota and billing.

To make this the default policy for the service, set the policy during the client initialization:

    let client = secret_manager::client::SecretManagerService::builder()
        .with_retry_policy(Aip194Strict)
        .build()
        .await?;

Then use the service as usual:

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(secret) = list.next().await {
        let secret = secret?;
        println!("  secret={}", secret.name);
    }

See below for the complete code.

Configuring the default retry policy with limits

The Aip194Strict policy does not limit the number of retry attempts or the time spent retrying requests. However, it can be decorated to set such limits. For example, you can limit both the number of attempts and the time spent in the retry loop using:

    let client = secret_manager::client::SecretManagerService::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(15)),
        )
        .build()
        .await?;

Requests work as usual too:

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(secret) = list.next().await {
        let secret = secret?;
        println!("  secret={}", secret.name);
    }

See below for the complete code.

Override the retry policy for one request

Sometimes applications need to override the retry policy for a specific request. For example, the application developer may know specific details of the service or application and determine it is safe to tolerate more errors.

For example, deleting a secret is idempotent, because it can only succeed once. But the client library assumes all delete operations are unsafe. The application can override the policy for one request:

    client
        .delete_secret()
        .set_name(format!("projects/{project_id}/secrets/{secret_id}"))
        .with_retry_policy(
            AlwaysRetry
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(15)),
        )
        .send()
        .await?;

See below for the complete code.

Configuring the default retry policy: complete code

pub async fn client_retry(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::paginator::ItemPaginator as _;
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_secretmanager_v1 as secret_manager;

    let client = secret_manager::client::SecretManagerService::builder()
        .with_retry_policy(Aip194Strict)
        .build()
        .await?;

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(secret) = list.next().await {
        let secret = secret?;
        println!("  secret={}", secret.name);
    }

    Ok(())
}

Configuring the default retry policy with limits: complete code

pub async fn client_retry_full(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::paginator::ItemPaginator as _;
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use google_cloud_secretmanager_v1 as secret_manager;
    use std::time::Duration;

    let client = secret_manager::client::SecretManagerService::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(15)),
        )
        .build()
        .await?;

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(secret) = list.next().await {
        let secret = secret?;
        println!("  secret={}", secret.name);
    }

    Ok(())
}

Override the retry policy for one request: complete code

use google_cloud_secretmanager_v1 as secret_manager;
pub async fn request_retry(
    client: &secret_manager::client::SecretManagerService,
    project_id: &str,
    secret_id: &str,
) -> crate::Result<()> {
    use google_cloud_gax::options::RequestOptionsBuilder;
    use google_cloud_gax::retry_policy::AlwaysRetry;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    client
        .delete_secret()
        .set_name(format!("projects/{project_id}/secrets/{secret_id}"))
        .with_retry_policy(
            AlwaysRetry
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(15)),
        )
        .send()
        .await?;

    Ok(())
}

Error handling

Sometimes applications need to branch based on the type and details of the error returned by the client library. This guide shows you how to write code to handle such errors.

Retryable errors: One of the most common reasons to handle errors in distributed systems is to retry requests that fail due to transient errors. The Google Cloud Client Libraries for Rust implement a policy-based retry loop. You only need to configure the policies to enable the retry loop, and the libraries implement common retry policies. Consult the Configuring Retry Policies section before implementing your own retry loop.

Prerequisites

The guide uses the Secret Manager service to demonstrate error handling, but the concepts apply to other services as well.

You may want to follow the service quickstart, which shows you how to enable the service and ensure that you've logged in and that your account has the necessary permissions.

For complete setup instructions for the Rust libraries, see Setting up your development environment.

Dependencies

Add the Secret Manager library to your Cargo.toml file:

cargo add google-cloud-secretmanager-v1

In addition, this guide uses crc32c to calculate the checksum:

cargo add crc32c

Motivation

In this guide you'll create a new secret version. Secret versions are contained in secrets. You must create the secret before adding secret versions. A common pattern in cloud services is to use a resource as if the container for it existed, and only create the container if there is an error. If the container exists most of the time, such an approach is more efficient than checking if the container exists before making the request. Checking if the container exists consumes more quota, results in more RPC charges, and is slower when the container already exists.

Handling the error

This section walks you through a function to update a secret. For the full code sample, see update_secret.

First make an attempt to create a new secret version:

    match update_attempt(&client, project_id, secret_id, data.clone()).await {

If update_attempt succeeds, you can just print the successful result and return:

        Ok(version) => {
            println!("new version is {}", version.name);
            Ok(version)
        }

The request may have failed for many reasons: because the connection dropped before the request was fully sent, or the connection dropped before the response was received, or because it was impossible to create the authentication tokens.

The retry policies can deal with most of these errors. Here we are interested only in errors returned by the service:

        Err(e) => {
            if let Some(status) = e.status() {

and then only in errors that correspond to a missing secret:

                use gax::error::rpc::Code;
                if status.code == Code::NotFound {

If this is a "not found" error, you can try to create the secret. This simply returns on failure:

                    let _ = create_secret(&client, project_id, secret_id).await?;

Assuming create_secret is successful, you can try to add the secret version again, this time just returning an error if anything fails:

                    let version = update_attempt(&client, project_id, secret_id, data).await?;
                    println!("new version is {}", version.name);
                    return Ok(version);

What's next

Learn more about error handling:


Code samples

update_secret

pub async fn update_secret(
    project_id: &str,
    secret_id: &str,
    data: Vec<u8>,
) -> crate::Result<sm::model::SecretVersion> {
    let client = sm::client::SecretManagerService::builder().build().await?;

    match update_attempt(&client, project_id, secret_id, data.clone()).await {
        Ok(version) => {
            println!("new version is {}", version.name);
            Ok(version)
        }
        Err(e) => {
            if let Some(status) = e.status() {
                use gax::error::rpc::Code;
                if status.code == Code::NotFound {
                    let _ = create_secret(&client, project_id, secret_id).await?;
                    let version = update_attempt(&client, project_id, secret_id, data).await?;
                    println!("new version is {}", version.name);
                    return Ok(version);
                }
            }
            Err(e.into())
        }
    }
}

update_attempt

async fn update_attempt(
    client: &sm::client::SecretManagerService,
    project_id: &str,
    secret_id: &str,
    data: Vec<u8>,
) -> gax::Result<sm::model::SecretVersion> {
    let checksum = crc32c::crc32c(&data) as i64;
    client
        .add_secret_version()
        .set_parent(format!("projects/{project_id}/secrets/{secret_id}"))
        .set_payload(
            sm::model::SecretPayload::new()
                .set_data(data)
                .set_data_crc32c(checksum),
        )
        .send()
        .await
}

create_secret

pub async fn create_secret(
    client: &sm::client::SecretManagerService,
    project_id: &str,
    secret_id: &str,
) -> gax::Result<sm::model::Secret> {
    use google_cloud_gax::options::RequestOptionsBuilder;
    use google_cloud_gax::retry_policy::AlwaysRetry;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    client
        .create_secret()
        .set_parent(format!("projects/{project_id}"))
        .with_retry_policy(
            AlwaysRetry
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(15)),
        )
        .set_secret_id(secret_id)
        .set_secret(
            sm::model::Secret::new()
                .set_replication(sm::model::Replication::new().set_replication(
                    sm::model::replication::Replication::Automatic(
                        sm::model::replication::Automatic::new().into(),
                    ),
                ))
                .set_labels([("integration-test", "true")]),
        )
        .send()
        .await
}

Examine error details

Some Google Cloud services include additional error details when requests fail. To help with any troubleshooting, the Google Cloud client libraries for Rust always include these details when errors are formatted using std::fmt::Display. Some applications may want to examine these details and change their behavior based on their contents.

This guide shows you how to examine the error details returned by Google Cloud services.

Prerequisites

This guide uses the Cloud Natural Language API to show error details, but the concepts apply to other services as well.

You may want to follow the service quickstart, which shows you how to enable the service and set up authentication.

For complete setup instructions for the Rust libraries, see Setting up your development environment.

Dependencies

As usual with Rust, you must declare the dependency in your Cargo.toml file:

cargo add google-cloud-language-v2

Examining error details

You'll create a request that intentionally results in an error, and then examine the error contents. First, create a client:

    use google_cloud_language_v2 as lang;
    let client = lang::client::LanguageService::builder().build().await?;

Then send a request. In this case, a key field is missing:

    let result = client
        .analyze_sentiment()
        .set_document(
            lang::model::Document::new()
                // Missing document contents
                // .set_content("Hello World!")
                .set_type(lang::model::document::Type::PlainText),
        )
        .send()
        .await;

Extract the error from the result, using standard Rust functions. The error type prints all the error details in human-readable form:

    let err = result.expect_err("the request should have failed");
    println!("\nrequest failed with error {err:#?}");

This should produce output similar to:

request failed with error Error {
    kind: Service {
        status_code: Some(
            400,
        ),
        headers: Some(
            {
                "vary": "X-Origin",
                "vary": "Referer",
                "vary": "Origin,Accept-Encoding",
                "content-type": "application/json; charset=UTF-8",
                "date": "Sat, 24 May 2025 17:19:49 GMT",
                "server": "scaffolding on HTTPServer2",
                "x-xss-protection": "0",
                "x-frame-options": "SAMEORIGIN",
                "x-content-type-options": "nosniff",
                "alt-svc": "h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000",
                "accept-ranges": "none",
                "transfer-encoding": "chunked",
            },
        ),
        status: Status {
            code: InvalidArgument,
            message: "One of content, or gcs_content_uri must be set.",
            details: [
                BadRequest(
                    BadRequest {
                        field_violations: [
                            FieldViolation {
                                field: "document.content",
                                description: "Must have some text content to annotate.",
                                reason: "",
                                localized_message: None,
                                _unknown_fields: {},
                            },
                        ],
                        _unknown_fields: {},
                    },
                ),
            ],
        },
    },
}

Programmatically examining the error details

Sometimes you may need to examine the error details programmatically. The rest of the example traverses the data structure and prints the most relevant fields.

Only errors returned by the service contain detailed information, so first query the error to see if it contains the correct error type. If it does, you can break down some top-level information about the error:

    if let Some(status) = err.status() {
        println!(
            "  status.code={}, status.message={}",
            status.code, status.message,
        );

And then iterate over all the details:

        for detail in status.details.iter() {
            use google_cloud_gax::error::rpc::StatusDetails;
            match detail {

The client libraries return a StatusDetails enum with the different types of error details. This example only examines BadRequest errors:

                StatusDetails::BadRequest(bad) => {

A BadRequest contains a list of fields that are in violation. You can iterate and print the details for each:

                    for f in bad.field_violations.iter() {
                        println!(
                            "  the request field {} has a problem: \"{}\"",
                            f.field, f.description
                        );
                    }

Such information can be useful during development. Other branches of StatusDetails such as QuotaFailure may be useful at runtime to throttle an application.

Expected output

Typically the output from the error details will look like so:

  status.code=400, status.message=One of content, or gcs_content_uri must be set., status.status=Some("INVALID_ARGUMENT")
  the request field document.content has a problem: "Must have some text content to annotate."

What's next


Examining error details: complete code

pub async fn examine_error_details() -> crate::Result<()> {
    use google_cloud_language_v2 as lang;
    let client = lang::client::LanguageService::builder().build().await?;

    let result = client
        .analyze_sentiment()
        .set_document(
            lang::model::Document::new()
                // Missing document contents
                // .set_content("Hello World!")
                .set_type(lang::model::document::Type::PlainText),
        )
        .send()
        .await;

    let err = result.expect_err("the request should have failed");
    println!("\nrequest failed with error {err:#?}");

    if let Some(status) = err.status() {
        println!(
            "  status.code={}, status.message={}",
            status.code, status.message,
        );
        for detail in status.details.iter() {
            use google_cloud_gax::error::rpc::StatusDetails;
            match detail {
                StatusDetails::BadRequest(bad) => {
                    for f in bad.field_violations.iter() {
                        println!(
                            "  the request field {} has a problem: \"{}\"",
                            f.field, f.description
                        );
                    }
                }
                _ => {
                    println!("  additional error details: {detail:?}");
                }
            }
        }
    }

    Ok(())
}

Handling binding errors

You might have tried to make a request and run into an error that looks like this:

Error: cannot find a matching binding to send the request: at least one of the
conditions must be met: (1) field `name` needs to be set and match the template:
'projects/*/secrets/*' OR (2) field `name` needs to be set and match the
template: 'projects/*/locations/*/secrets/*'

This is a binding error, and this guide explains how to troubleshoot binding errors.

What causes a binding error

The Google Cloud Client Libraries for Rust primarily use HTTP to send requests to Google Cloud services. An HTTP request uses a Uniform Resource Identifier (URI) to specify a resource.

Some RPCs correspond to multiple URIs. The contents of the request determine which URI is used.

The client library considers all possible URIs, and only returns a binding error if no URIs work. Typically this happens when a field is either missing or in an invalid format.

The example error above was produced by trying to get a resource without naming the resource. Specifically, the name field on a GetSecretRequest was required but not set.

    let secret = client
        .get_secret()
        //.set_name("projects/my-project/secrets/my-secret")
        .send()
        .await;

How to fix it

In this case, to fix the error you'd set the name field to something matching one of the templates shown in the error message:

  • 'projects/*/secrets/*'
  • 'projects/*/locations/*/secrets/*'

Either allows the client library to make a request to the server:

    let secret = client
        .get_secret()
        .set_name("projects/my-project/secrets/my-secret")
        .send()
        .await;

or

    let secret = client
        .get_secret()
        .set_name("projects/my-project/locations/us-central1/secrets/my-secret")
        .send()
        .await;

Interpreting templates

The error message for a binding error includes a number of template strings showing possible values for the request fields. Most template strings include * and ** as wildcards to match the field values.

Single wildcard

The * wildcard alone means a non-empty string without a /. It can be thought of as the regex [^/]+.

Here are some examples:

TemplateInputMatch?
"*""simple-string-123"true
"projects/*""projects/p"true
"projects/*/locations""projects/p/locations"true
"projects/*/locations/*""projects/p/locations/l"true
"*""" (empty)false
"*""string/with/slashes"false
"projects/*""projects/" (empty)false
"projects/*""projects/p/" (extra slash)false
"projects/*""projects/p/locations/l"false
"projects/*/locations""projects/p"false
"projects/*/locations""projects/p/locations/l"false

Double wildcard

Less common is the ** wildcard, which means any string. The string can be empty or contain any number of /'s. It can be thought of as the regex .*.

Also, when a template ends in /**, that initial slash is optionally included.

TemplateInputMatch?
"**"""true
"**""simple-string-123"true
"**""string/with/slashes"true
"projects/*/**""projects/p"true
"projects/*/**""projects/p/locations"true
"projects/*/**""projects/p/locations/l"true
"projects/*/**""locations/l"false
"projects/*/**""projects//locations/l"false

Inspecting the error

If you need to inspect the error programmatically, you can do so by checking that it is a binding error, then downcasting it to a BindingError.

    let secret = client
        .get_secret()
        //.set_name("projects/my-project/secrets/my-secret")
        .send()
        .await;

    use gax::error::binding::BindingError;
    let e = secret.unwrap_err();
    assert!(e.is_binding(), "{e:?}");
    assert!(e.source().is_some(), "{e:?}");
    let _ = e
        .source()
        .and_then(|e| e.downcast_ref::<BindingError>())
        .expect("should be a BindingError");

Working with list operations

Some services return potentially large lists of items, such as rows or resource descriptions. To keep CPU and memory usage under control, services return these resources in pages: smaller subsets of the items with a continuation token to request the next subset.

Iterating over items by page can be tedious. The client libraries provide adapters to convert the pages into asynchronous iterators. This guide shows you how to work with these adapters.

Prerequisites

This guide uses the Secret Manager service to demonstrate list operations, but the concepts apply to other services as well.

You may want to follow the service quickstart, which shows you how to enable the service and ensure that you've logged in and that your account has the necessary permissions.

For complete setup instructions for the Rust libraries, see Setting up your development environment.

Dependencies

Add the Secret Manager library to your Cargo.toml file:

cargo add google-cloud-secretmanager-v1

Iterating list methods

To help iterate the items in a list method, the APIs return an implementation of the ItemPaginator trait. Introduce it into scope via a use declaration:

    use google_cloud_gax::paginator::ItemPaginator as _;

To iterate the items, use the by_item function.

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item();
    while let Some(secret) = list.next().await {
        let secret = secret?;
        println!("  secret={}", secret.name)
    }

In rare cases, pages might contain extra information that you need access to. Or you may need to checkpoint your progress across processes. In these cases, you can iterate over full pages instead of individual items.

First introduce Paginator into scope via a use declaration:

    use google_cloud_gax::paginator::Paginator as _;

Then iterate over the pages using by_page:

    let mut list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_page();
    while let Some(page) = list.next().await {
        let page = page?;
        println!("  next_page_token={}", page.next_page_token);
        page.secrets
            .into_iter()
            .for_each(|secret| println!("    secret={}", secret.name));
    }

Working with futures::Stream

You may want to use these APIs in the larger Rust ecosystem of asynchronous streams, such as tokio::Stream. This is readily done, but you must first enable the unstable-streams feature in the google_cloud_gax crate:

cargo add google-cloud-gax --features unstable-stream

The name of this feature is intended to convey that we consider these APIs unstable, because they are! You should only use them if you are prepared to deal with any breaks that result from incompatible changes to the futures::Stream trait.

The following examples also use the futures::stream::StreamExt trait, which you enable by adding the futures crate.

cargo add futures

Add the required use declarations:

    use futures::stream::StreamExt;
    use google_cloud_gax::paginator::ItemPaginator as _;

Then use the into_stream function to convert ItemPaginator into a futures::Stream of items.

    let list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_item()
        .into_stream();
    list.map(|secret| -> gax::Result<()> {
        println!("  secret={}", secret?.name);
        Ok(())
    })
    .fold(Ok(()), async |acc, result| -> gax::Result<()> {
        acc.and(result)
    })
    .await?;

Similarly, you can use the into_stream function to convert Paginator into a futures::Stream of pages.

    let list = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .by_page()
        .into_stream();
    list.enumerate()
        .map(|(index, page)| -> gax::Result<()> {
            println!("page={}, next_page_token={}", index, page?.next_page_token);
            Ok(())
        })
        .fold(Ok(()), async |acc, result| -> gax::Result<()> {
            acc.and(result)
        })
        .await?;

Resuming list methods by setting next page token

In some cases, such as an interrupted list operation, you can set the next page token to resume paginating from a specific page.

    let page = client
        .list_secrets()
        .set_parent(format!("projects/{project_id}"))
        .send()
        .await;
    let page = page?;
    let mut next_page_token = page.next_page_token.clone();
    page.secrets
        .into_iter()
        .for_each(|secret| println!("    secret={}", secret.name));

    while !next_page_token.is_empty() {
        println!("  next_page_token={next_page_token}");

        let page = client
            .list_secrets()
            .set_parent(format!("projects/{project_id}"))
            .set_page_token(next_page_token)
            .send()
            .await;
        let page = page?;
        next_page_token = page.next_page_token.clone();

        page.secrets
            .into_iter()
            .for_each(|secret| println!("    secret={}", secret.name));
    }

Additional paginator technical details

The standard Google API List method follows the pagination guideline defined by AIP-158. Each call to a List method for a resource returns a page of resource items (e.g. secrets) along with a next-page token that can be passed to the List method to retrieve the next page.

The Google Cloud Client Libraries for Rust provide an adapter to convert the list RPCs as defined by AIP-4233 into a streams that can be iterated over in an async fashion.

What's next

Learn more about working with the Cloud Client Libraries for Rust:

Working with long-running operations

Occasionally, an API may need to expose a method that takes a significant amount of time to complete. In these situations, it's often a poor user experience to simply block while the task runs. It's usually better to return some kind of promise to the user and allow the user to check back later.

The Google Cloud Client Libraries for Rust provide helpers to work with these long-running operations (LROs). This guide shows you how to start LROs and wait for their completion.

Prerequisites

The guide uses the Speech-To-Text V2 service to keep the code snippets concrete. The same ideas work for any other service using LROs.

We recommend that you first follow one of the service guides, such as Transcribe speech to text by using the command line. These guides cover critical topics such as ensuring your project has the API enabled, your account has the right permissions, and how to set up billing for your project (if needed). Skipping the service guides may result in problems that are hard to diagnose.

For complete setup instructions for the Rust libraries, see Setting up your development environment.

Dependencies

Declare Google Cloud dependencies in your Cargo.toml file:

cargo add google-cloud-speech-v2 google-cloud-lro google-cloud-longrunning

You'll also need several tokio features:

cargo add tokio --features full,macros

Starting a long-running operation

To start a long-running operation, you'll initialize a client and then make the RPC. But first, add some use declarations to avoid the long package names:

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

Now create the client:

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

You'll use batch recognize for this example. While this is designed for long audio files, it works well with small files too.

In the Rust client libraries, each request is represented by a method that returns a request builder. First, call the right method on the client to create the request builder. You'll use the default recognizer (_) in the global region.

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))

Then initialize the request to use a publicly available audio file:

        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])

Configure the request to return the transcripts inline:

        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )

Then configure the service to transcribe to US English, using the short model and some other default configuration:

        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )

Make the request and wait for an Operation to be returned. This Operation acts as a promise for the result of the long-running request:

        .send()
        .await?;

Finally, poll the promise until it completes:

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

You'll examine the manually_poll_lro() function in the Manually polling a long-running operation section.

You can find the full function below.

Automatically polling a long-running operation

To configure automatic polling, you prepare the request just like you did to start a long-running operation. The difference comes at the end, where instead of sending the request to get the Operation promise:

        .send()
        .await?;

... you create a Poller and wait until it is done:

        .poller()
        .until_done()
        .await?;

Let's review the code step-by-step.

First, introduce the trait in scope via a use declaration:

    use google_cloud_lro::Poller;

Then initialize the client and prepare the request as before:

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )

And then poll until the operation is completed and print the result:

        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

You can find the full function below.

Polling a long-running operation

While .until_done() is convenient, it omits some information: long-running operations may report partial progress via a "metadata" attribute. If your application requires such information, you need to use the poller directly:

    let mut poller = client
        .batch_recognize(/* stuff */)
        /* more stuff */
        .poller();

Then use the poller in a loop:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Note how this loop explicitly waits before polling again. The polling period depends on the specific operation and its payload. You should consult the service documentation and/or experiment with your own data to determine a good value.

The poller uses a policy to determine what polling errors are transient and may resolve themselves. The Configuring polling policies chapter covers this topic in detail.

You can find the full function below.

Manually polling a long-running operation

In general, we recommend that you use the previous two approaches in your application. Alternatively, you can manually poll a long-running operation, but this can be quite tedious, and it is easy to get the types wrong. If you do need to manually poll a long-running operation, this section walks you through the required steps. You may want to read the Operation message reference documentation, as some of the fields and types are used below.

Recall that you started the long-running operation using the client:

    let mut operation = client
        .batch_recognize(/* stuff */)
        /* more stuff */
        .send()
        .await?;

You are going to start a loop to poll the operation, and you need to check if the operation completed immediately (this is rare but does happen). The done field indicates if the operation completed:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

In most cases, if the operation is done it contains a result. However, the field is optional because the service could return done as true and no result: maybe the operation deletes resources and a successful completion has no return value. In this example using the Speech-to-Text service, you can treat this as an error:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Starting a long-running operation successfully does not guarantee that it will complete successfully. The result may be an error or a valid response. You need to check for both. First check for errors:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

The error type is a Status message type. This does not implement the standard Error interface. You need to manually convert it to a valid error. You can use Error::service to perform this conversion.

Assuming the result is successful, you need to extract the response type. You can find this type in the documentation for the LRO method, or by reading the service API documentation:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Note that extraction of the value may fail if the type does not match what the service sent.

All types in Google Cloud may add fields and branches in the future. While this is unlikely for a common type such as Operation, it happens frequently for most service messages. The Google Cloud Client Libraries for Rust mark all structs and enums as #[non_exhaustive] to signal that such changes are possible. In this case, you must handle this unexpected case:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

If the operation has not completed, then it may contain some metadata. Some services just include initial information about the request, while other services include partial progress reports. You can choose to extract and report this metadata:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

As the operation has not completed, you need to wait before polling again. Consider adjusting the polling period, maybe using a form of truncated exponential backoff. This example simply polls every 500ms:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Then you can poll the operation to get its new status:

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

For simplicity, the example ignores all errors. In your application you may choose to treat only a subset of the errors as non-recoverable, and may want to limit the number of polling attempts if these fail.

You can find the full function below.

What's next

Starting a long-running operation: complete code

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

Automatically polling a long-running operation: complete code

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Polling a long-running operation: complete code

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Manually polling a long-running operation: complete code

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;

pub async fn start(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let operation = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .send()
        .await?;
    println!("LRO started, response={operation:?}");

    let response = manually_poll_lro(client, operation).await;
    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn automatic(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

pub async fn polling(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::retry_policy::Aip194Strict;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

    use google_cloud_lro::{Poller, PollingResult};

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            Aip194Strict
                .with_attempt_limit(5)
                .with_time_limit(Duration::from_secs(30)),
        )
        .build()
        .await?;

    let mut poller = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller();

    while let Some(p) = poller.poll().await {
        match p {
            PollingResult::Completed(r) => {
                println!("LRO completed, response={r:?}");
            }
            PollingResult::InProgress(m) => {
                println!("LRO in progress, metadata={m:?}");
            }
            PollingResult::PollingError(e) => {
                println!("Transient error polling the LRO: {e}");
            }
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
    }

    Ok(())
}

pub async fn manually_poll_lro(
    client: speech::client::Speech,
    operation: longrunning::model::Operation,
) -> crate::Result<speech::model::BatchRecognizeResponse> {
    let mut operation = operation;
    loop {
        if operation.done {
            match &operation.result {
                None => {
                    return Err("missing result for finished operation".into());
                }
                Some(r) => {
                    return match r {
                        longrunning::model::operation::Result::Error(e) => {
                            Err(format!("{e:?}").into())
                        }
                        longrunning::model::operation::Result::Response(any) => {
                            let response = any.to_msg::<speech::model::BatchRecognizeResponse>()?;
                            Ok(response)
                        }
                        _ => Err(format!("unexpected result branch {r:?}").into()),
                    };
                }
            }
        }
        if let Some(any) = &operation.metadata {
            let metadata = any.to_msg::<speech::model::OperationMetadata>()?;
            println!("LRO in progress, metadata={metadata:?}");
        }
        tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        if let Ok(attempt) = client
            .get_operation()
            .set_name(&operation.name)
            .send()
            .await
        {
            operation = attempt;
        }
    }
}

Configuring polling policies

The Google Cloud Client Libraries for Rust provide helper functions to simplify waiting and monitoring the progress of LROs (Long-Running Operations). These helpers use policies to configure the polling frequency and to determine what polling errors are transient and may be ignored until the next polling event.

This guide will walk you through the configuration of these policies for all the long-running operations started by a client, or just for one specific request.

There are two different policies controlling the behavior of the LRO loops:

  • The polling backoff policy controls how long the loop waits before polling the status of a LRO that is still in progress.
  • The polling error policy controls what to do on an polling error. Some polling errors are unrecoverable, and indicate that the operation was aborted or the caller has no permissions to check the status of the LRO. Other polling errors are transient, and indicate a temporary problem in the client network or the service.

Each one of these policies can be set independently, and each one can be set for all the LROs started on a client or changed for just one request.

Prerequisites

The guide uses the Speech-To-Text V2 service to keep the code snippets concrete. The same ideas work for any other service using LROs.

We recommend you first follow one of the service guides, such as Transcribe speech to text by using the command line. These guides will cover critical topics such as ensuring your project has the API enabled, your account has the right permissions, and how to set up billing for your project (if needed). Skipping the service guides may result in problems that are hard to diagnose.

Dependencies

As it is usual with Rust, you must declare the dependency in your Cargo.toml file. We use:

cargo add google-cloud-speech-v2 google-cloud-lro

Configuring the polling frequency for all requests in a client

If you are planning to use the same polling backoff policy for all (or even most) requests with the same client then consider setting this as a client option.

To configure the polling frequency you use a type implementing the PollingBackoffPolicy trait. The client libraries provide ExponentialBackoff:

    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;

Then initialize the client with the configuration you want:

    let client = speech::client::Speech::builder()
        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )
        .build()
        .await?;

Unless you override the policy with a per-request setting this policy will be in effect for any long-running operation started with the client. In this example, if you make a call such as:

    let mut operation = client
        .batch_recognize(/* stuff */)
        /* more stuff */
        .send()
        .await?;

The client library will first wait for 500ms, after the first polling attempt, then for 1,000ms (or 1s) for the second attempt, and sub-sequent attempts will wait 2s, 4s, 8s and then all attempts will wait 10s.

See below for the complete code.

Configuring the polling frequency for a specific request

As described in the previous section. We need a type implementing the PollingBackoffPolicy trait to configure the polling frequency. We will also use ExponentialBackoff in this example:

    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
    use std::time::Duration;

The configuration of the request will require bringing a trait within scope:

    use google_cloud_gax::options::RequestOptionsBuilder;

You create the request builder as usual:

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))

And then configure the polling backoff policy:

        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )

You can issue this request as usual. For example:

        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

See below for the complete code.

Configuring the retryable polling errors for all requests in a client

To configure the retryable errors we need to use a type implementing the PollingErrorPolicy trait. The client libraries provide a number of them, a conservative choice is Aip194Strict:

    use google_cloud_gax::polling_error_policy::Aip194Strict;
    use google_cloud_gax::polling_error_policy::PollingErrorPolicyExt;
    use google_cloud_gax::retry_policy;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

If you are planning to use the same polling policy for all (or even most) requests with the same client then consider setting this as a client option.

Add the polling policies that you will use for all long running operations:

    let builder = speech::client::Speech::builder().with_polling_error_policy(
        Aip194Strict
            .with_attempt_limit(100)
            .with_time_limit(Duration::from_secs(300)),
    );

You can also add retry policies to handle errors in the initial request:

    let client = builder
        .with_retry_policy(
            retry_policy::Aip194Strict
                .with_attempt_limit(100)
                .with_time_limit(Duration::from_secs(300)),
        )
        .build()
        .await?;

Unless you override the policy with a per-request setting this policy will be in effect for any long-running operation started with the client. In this example, if you make a call such as:

    let mut operation = client
        .batch_recognize(/* stuff */)
        /* more stuff */
        .send()
        .await?;

The client library will only treat UNAVAILABLE (see AIP-194) as a retryable error, and will stop polling after 100 attempts or 300 seconds, whichever comes first.

See below for the complete code.

Configuring the retryable polling errors for a specific request

To configure the retryable errors we need to use a type implementing the PollingErrorPolicy trait. The client libraries provide a number of them, a conservative choice is Aip194Strict:

    use google_cloud_gax::polling_error_policy::Aip194Strict;
    use google_cloud_gax::polling_error_policy::PollingErrorPolicyExt;
    use google_cloud_gax::retry_policy;
    use google_cloud_gax::retry_policy::RetryPolicyExt;
    use std::time::Duration;

The configuration of the request will require bringing a trait within scope:

    use google_cloud_gax::options::RequestOptionsBuilder;

You create the request builder as usual:

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))

And then configure the polling backoff policy:

        .with_polling_error_policy(
            Aip194Strict
                .with_attempt_limit(100)
                .with_time_limit(Duration::from_secs(300)),
        )
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

You can issue this request as usual. For example:

        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

Consider adding a retry policy in case the initial request to start the LRO fails:

    let client = speech::client::Speech::builder()
        .with_retry_policy(
            retry_policy::Aip194Strict
                .with_attempt_limit(100)
                .with_time_limit(Duration::from_secs(300)),
        )
        .build()
        .await?;

See below for the complete code.

Configuring the polling frequency for all requests in a client: complete code

pub async fn client_backoff(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
    use google_cloud_lro::Poller;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

Configuring the polling frequency for a specific request: complete code

pub async fn rpc_backoff(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
    use std::time::Duration;
    use google_cloud_gax::options::RequestOptionsBuilder;
    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder().build().await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

Configuring the retryable polling errors for all requests in a client: complete code

pub async fn client_backoff(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
    use google_cloud_lro::Poller;
    use std::time::Duration;

    let client = speech::client::Speech::builder()
        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )
        .build()
        .await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

Configuring the retryable polling errors for a specific request: complete code

pub async fn rpc_backoff(project_id: &str) -> crate::Result<()> {
    use google_cloud_gax::exponential_backoff::ExponentialBackoffBuilder;
    use std::time::Duration;
    use google_cloud_gax::options::RequestOptionsBuilder;
    use google_cloud_lro::Poller;

    let client = speech::client::Speech::builder().build().await?;

    let response = client
        .batch_recognize()
        .set_recognizer(format!(
            "projects/{project_id}/locations/global/recognizers/_"
        ))
        .with_polling_backoff_policy(
            ExponentialBackoffBuilder::new()
                .with_initial_delay(Duration::from_millis(250))
                .with_maximum_delay(Duration::from_secs(10))
                .build()?,
        )
        .set_files([speech::model::BatchRecognizeFileMetadata::new()
            .set_uri("gs://cloud-samples-data/speech/hello.wav")])
        .set_recognition_output_config(
            speech::model::RecognitionOutputConfig::new()
                .set_inline_response_config(speech::model::InlineOutputConfig::new()),
        )
        .set_processing_strategy(
            speech::model::batch_recognize_request::ProcessingStrategy::DynamicBatching,
        )
        .set_config(
            speech::model::RecognitionConfig::new()
                .set_language_codes(["en-US"])
                .set_model("short")
                .set_auto_decoding_config(speech::model::AutoDetectDecodingConfig::new()),
        )
        .poller()
        .until_done()
        .await?;

    println!("LRO completed, response={response:?}");

    Ok(())
}

How to write tests using a client

The Google Cloud Client Libraries for Rust provide a way to stub out the real client implementations, so a mock can be injected for testing.

Applications can use mocks to write controlled, reliable unit tests that do not involve network calls, and do not incur billing.

This guide shows how.

Dependencies

There are several mocking frameworks in Rust. This guide uses mockall, which seems to be the most popular.

cargo add --dev mockall

This guide will use a Speech client. Note that the same ideas in this guide apply to all of the clients, not just the Speech client.

We declare the dependency in our Cargo.toml. Yours will be similar, but without the custom path.

cargo add google-cloud-speech-v2 google-cloud-lro

Mocking a client

First, some use declarations to simplify the code:

    use google_cloud_gax as gax;
    use google_cloud_speech_v2 as speech;

Let's assume our application has a function that uses the Speech client to make an RPC, and process the response from the server.

    // An example application function.
    //
    // It makes an RPC, setting some field. In this case, it is the `GetRecognizer`
    // RPC, setting the name field.
    //
    // It processes the response from the server. In this case, it extracts the
    // display name of the recognizer.
    async fn my_application_function(client: &speech::client::Speech) -> gax::Result<String> {
        client
            .get_recognizer()
            .set_name("invalid-test-recognizer")
            .send()
            .await
            .map(|r| r.display_name)
    }

We want to test how our code handles different responses from the service.

First we will define the mock class. This class implements the speech::stub::Speech trait.

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn get_recognizer(&self, req: speech::model::GetRecognizerRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<speech::model::Recognizer>>;
        }
    }

Next, we create an instance of the mock. Note that the mockall::mock! macro prepends a Mock prefix to the name of our struct from above.

        let mut mock = MockSpeech::new();

Next we will set expectations on the mock. We expect GetRecognizer to be called, with a particular name.

If that happens, we will simulate a successful response from the service.

        mock.expect_get_recognizer()
            .withf(move |r, _|
                // Optionally, verify fields in the request.
                r.name == "invalid-test-recognizer")
            .return_once(|_, _| {
                Ok(gax::response::Response::from(
                    speech::model::Recognizer::new().set_display_name("test-display-name"),
                ))
            });

Now we are ready to create a Speech client with our mock.

        let client = speech::client::Speech::from_stub(mock);

Finally, we are ready to call our function...

        let display_name = my_application_function(&client).await?;

... and verify the results.

        assert_eq!(display_name, "test-display-name");

Simulating errors

Simulating errors is no different than simulating successes. We just need to modify the result returned by our mock.

        mock.expect_get_recognizer().return_once(|_, _| {
            // This time, return an error.
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::NotFound)
                .set_message("Resource not found");
            Err(Error::service(status))
        });

Note that a client built from_stub() does not have an internal retry loop. It returns all errors from the stub directly to the application.


Full program

Putting all this code together into a full program looks as follows:

#[cfg(test)]
mod tests {
    use google_cloud_gax as gax;
    use google_cloud_speech_v2 as speech;
    type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

    // An example application function.
    //
    // It makes an RPC, setting some field. In this case, it is the `GetRecognizer`
    // RPC, setting the name field.
    //
    // It processes the response from the server. In this case, it extracts the
    // display name of the recognizer.
    async fn my_application_function(client: &speech::client::Speech) -> gax::Result<String> {
        client
            .get_recognizer()
            .set_name("invalid-test-recognizer")
            .send()
            .await
            .map(|r| r.display_name)
    }

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn get_recognizer(&self, req: speech::model::GetRecognizerRequest, _options: gax::options::RequestOptions) -> gax::Result<gax::response::Response<speech::model::Recognizer>>;
        }
    }

    #[tokio::test]
    async fn basic_success() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_get_recognizer()
            .withf(move |r, _|
                // Optionally, verify fields in the request.
                r.name == "invalid-test-recognizer")
            .return_once(|_, _| {
                Ok(gax::response::Response::from(
                    speech::model::Recognizer::new().set_display_name("test-display-name"),
                ))
            });

        // Create a client, implemented by the mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function.
        let display_name = my_application_function(&client).await?;

        // Verify the final result of the RPC.
        assert_eq!(display_name, "test-display-name");

        Ok(())
    }

    #[tokio::test]
    async fn basic_fail() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_get_recognizer().return_once(|_, _| {
            // This time, return an error.
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::NotFound)
                .set_message("Resource not found");
            Err(Error::service(status))
        });

        // Create a client, implemented by the mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function.
        let display_name = my_application_function(&client).await;

        // Verify the final result of the RPC.
        assert!(display_name.is_err());

        Ok(())
    }
}

How to write tests for long-running operations

The Google Cloud client libraries for Rust have helpers that simplify interaction with long-running operations (henceforth, LROs).

Simulating the behavior of LROs in tests involves understanding the details these helpers hide. This guide shows how to do that.

Prerequisites

This guide assumes you are familiar with the previous chapters:

Tests for automatic polling

Let's say our application code awaits lro::Poller::until_done(). In previous sections, we called this "automatic polling".

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Note that our application only cares about the final result of the LRO. We do not need to test how it handles intermediate results from polling the LRO. Our tests can simply return the final result of the LRO from the mock.

Creating the longrunning::model::Operation

Let's say we want our call to result in the following response.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

You may have noticed that the stub returns a longrunning::model::Operation, not a BatchRecognizeResponse. We need to pack our desired response into the Operation::result.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Note also that we set the done field to true. This indicates to the Poller that the operation has completed, thus ending the polling loop.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Test code

Now we are ready to write our test.

First we define our mock class, which implements the speech::stub::Speech trait.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Now in our test we create our mock, and set expectations on it.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Finally, we create a client from the mock, call our function, and verify the response.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Tests for manual polling with intermediate metadata

Let's say our application code manually polls, and does some processing on partial updates.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

We want to simulate how our application acts when it receives intermediate metadata. We can achieve this by returning in-progress operations from our mock.

Creating the longrunning::model::Operation

The BatchRecognize RPC returns partial results in the form of a speech::model::OperationMetadata. Like before, we will need to pack this into the returned longrunning::model::Operation, but this time into the Operation::metadata field.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

Test code

First we define our mock class, which implements the speech::stub::Speech trait. Note that we override get_operation(). We will see why shortly.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

Now in our test we create our mock, and set expectations on it.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

These expectations will return partial results (25%, 50%, 75%), then return our desired final outcome.

Now a few things you probably noticed.

  1. The first expectation is set on batch_recognize(), whereas all subsequent expectations are set on get_operation().

    The initial BatchRecognize RPC starts the LRO on the server-side. The server returns some identifier for the LRO. This is the name field which is omitted from the test code, for simplicity.

    From then on, the client library just polls the status of that LRO. It does this using the GetOperation RPC.

    That is why we set expectations on different RPCs for the initial response vs. all subsequent responses.

  2. Expectations are set in a sequence.

    This allows mockall to verify the order of the calls. It is also necessary to determine which expect_get_operation is matched.

Finally, we create a client from the mock, call our function, and verify the response.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

Simulating errors

Errors can arise in an LRO from a few places.

If your application uses automatic polling, the following cases are all equivalent: until_done() returns the error in the Result, regardless of where it originated. Simulating an error starting an LRO will yield the simplest test.

Note that the stubbed out client does not have a retry or polling policy. In all cases, the polling loop will terminate on the first error, even if the error is typically considered transient.

Simulating an error starting an LRO

The simplest way to simulate an error is to have the initial request fail with an error.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

For manual polling, an error starting an LRO is returned via the completed branch. This ends the polling loop.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

Simulating an LRO resulting in an error

If you need to simulate an LRO resulting in an error, after intermediate metadata is returned, we need to return the error in the final longrunning::model::Operation.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

We set our expectations to return the Operation from get_operation as before.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

An LRO ending in an error will be returned via the completed branch. This ends the polling loop.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

Simulating a polling error

Polling loops can also exit because the polling policy has been exhausted. When this happens, the client library can not say definitively whether the LRO has completed or not.

If your application has custom logic to deal with this case, we can exercise it by returning an error from the get_operation expectation.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

An LRO ending with a polling error will be returned via the polling error branch.

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

Automatic polling - Full test

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::Operation;
use longrunning::model::operation::Result as OperationResult;
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse};

// Example application code that is under test
mod my_application {
    use super::*;

    // An example application function that automatically polls.
    //
    // It starts an LRO, awaits the result, and processes it.
    pub async fn my_automatic_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> Result<Option<wkt::Duration>> {
        use google_cloud_lro::Poller;
        client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller()
            .until_done()
            .await
            .map(|r| r.total_billed_duration)
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(response: &BatchRecognizeResponse) -> Result<Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn automatic_polling() -> Result<()> {
        // Create a mock, and set expectations on it.
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .return_once(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which automatically polls.
        let billed_duration = my_automatic_poller(&client, "my-project").await?;

        // Verify the final result of the LRO.
        assert_eq!(billed_duration, expected_duration());

        Ok(())
    }
}

Manual polling with intermediate metadata - Full test

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, BatchRecognizeResponse, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn expected_duration() -> Option<wkt::Duration> {
        Some(wkt::Duration::clamp(100, 0))
    }

    fn expected_response() -> BatchRecognizeResponse {
        BatchRecognizeResponse::new().set_or_clear_total_billed_duration(expected_duration())
    }

    fn make_finished_operation(
        response: &BatchRecognizeResponse,
    ) -> Result<gax::response::Response<Operation>> {
        let any = wkt::Any::from_msg(response).expect("test message should succeed");
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Response(any.into()));
        Ok(Response::from(operation))
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn manual_polling_with_metadata() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_finished_operation(&expected_response()));

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert_eq!(result.billed_duration?, expected_duration());

        Ok(())
    }
}

Simulating errors - Full tests

// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Examples showing how to simulate LROs in tests.

use gax::Result;
use gax::response::Response;
use google_cloud_gax as gax;
use google_cloud_longrunning as longrunning;
use google_cloud_rpc as rpc;
use google_cloud_speech_v2 as speech;
use google_cloud_wkt as wkt;
use longrunning::model::operation::Result as OperationResult;
use longrunning::model::{GetOperationRequest, Operation};
use speech::model::{BatchRecognizeRequest, OperationMetadata};

// Example application code that is under test
mod my_application {
    use super::*;

    pub struct BatchRecognizeResult {
        pub progress_updates: Vec<i32>,
        pub billed_duration: Result<Option<wkt::Duration>>,
    }

    // An example application function that manually polls.
    //
    // It starts an LRO. It consolidates the polling results, whether full or
    // partial.
    //
    // In this case, it is the `BatchRecognize` RPC. If we get a partial update,
    // we extract the `progress_percent` field. If we get a final result, we
    // extract the `total_billed_duration` field.
    pub async fn my_manual_poller(
        client: &speech::client::Speech,
        project_id: &str,
    ) -> BatchRecognizeResult {
        use google_cloud_lro::{Poller, PollingResult};
        let mut progress_updates = Vec::new();
        let mut poller = client
            .batch_recognize()
            .set_recognizer(format!(
                "projects/{project_id}/locations/global/recognizers/_"
            ))
            .poller();
        while let Some(p) = poller.poll().await {
            match p {
                PollingResult::Completed(r) => {
                    let billed_duration = r.map(|r| r.total_billed_duration);
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration,
                    };
                }
                PollingResult::InProgress(m) => {
                    if let Some(metadata) = m {
                        // This is a silly application. Your application likely
                        // performs some task immediately with the partial
                        // update, instead of storing it for after the operation
                        // has completed.
                        progress_updates.push(metadata.progress_percent);
                    }
                }
                PollingResult::PollingError(e) => {
                    return BatchRecognizeResult {
                        progress_updates,
                        billed_duration: Err(e),
                    };
                }
            }
            tokio::time::sleep(std::time::Duration::from_millis(500)).await;
        }

        // We can only get here if `poll()` returns `None`, but it only returns
        // `None` after it returned `PollingResult::Completed`. Therefore this
        // is never reached.
        unreachable!("loop should exit via the `Completed` branch.");
    }
}

#[cfg(test)]
mod tests {
    use super::my_application::*;
    use super::*;

    mockall::mock! {
        #[derive(Debug)]
        Speech {}
        impl speech::stub::Speech for Speech {
            async fn batch_recognize(&self, req: BatchRecognizeRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
            async fn get_operation(&self, req: GetOperationRequest, _options: gax::options::RequestOptions) -> Result<Response<Operation>>;
        }
    }

    fn make_partial_operation(progress: i32) -> Result<Response<Operation>> {
        let metadata = OperationMetadata::new().set_progress_percent(progress);
        let any = wkt::Any::from_msg(&metadata).expect("test message should succeed");
        let operation = Operation::new().set_metadata(any);
        Ok(Response::from(operation))
    }

    fn make_failed_operation(status: rpc::model::Status) -> Result<Response<Operation>> {
        let operation = Operation::new()
            .set_done(true)
            .set_result(OperationResult::Error(status.into()));
        Ok(Response::from(operation))
    }

    #[tokio::test]
    async fn error_starting_lro() -> Result<()> {
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize().return_once(|_, _| {
            use gax::error::Error;
            use gax::error::rpc::{Code, Status};
            let status = Status::default()
                .set_code(Code::Aborted)
                .set_message("Resource exhausted");
            Err(Error::service(status))
        });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the the final result.
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn lro_ending_in_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(50));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(75));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                // This is a common error for `Create*` RPCs, which are often
                // LROs. It is less applicable to `BatchRecognize` in practice.
                let status = rpc::model::Status::default()
                    .set_code(gax::error::rpc::Code::AlreadyExists as i32)
                    .set_message("resource already exists");
                make_failed_operation(status)
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25, 50, 75]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }

    #[tokio::test]
    async fn polling_loop_error() -> Result<()> {
        let mut seq = mockall::Sequence::new();
        let mut mock = MockSpeech::new();
        mock.expect_batch_recognize()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| make_partial_operation(25));
        mock.expect_get_operation()
            .once()
            .in_sequence(&mut seq)
            .returning(|_, _| {
                use gax::error::Error;
                use gax::error::rpc::{Code, Status};
                let status = Status::default()
                    .set_code(Code::Aborted)
                    .set_message("Operation was aborted");
                Err(Error::service(status))
            });

        // Create a client, implemented by our mock.
        let client = speech::client::Speech::from_stub(mock);

        // Call our function which manually polls.
        let result = my_manual_poller(&client, "my-project").await;

        // Verify the partial metadata updates, and the final result.
        assert_eq!(result.progress_updates, [25]);
        assert!(result.billed_duration.is_err());

        Ok(())
    }
}

Working with enums

This guide will show you how to use enumerations in the Google Cloud client libraries for Rust, including working with enumeration values introduced after the library was released.

Background

Google Cloud services use enumerations for fields that only accept or provide a discrete and limited set of values. While at any point in time the set of allowed values is known, this list may change over time.

The client libraries are prepared to receive and send enumerations, and support working with values introduced after the library release.

Prerequisites

This guide does not make any calls to Google Cloud services. You can run the examples without having a project or account in Google Cloud. The guide will use the client library for Secret Manager. The same principles apply to any other enumeration in any other client library.

Dependencies

As it is usual with Rust, you must declare the dependency in your Cargo.toml file. We use:

cargo add google-cloud-secretmanager-v1 serde_json

Handling known values

When using known values, you can use the enumeration as usual:

        use google_cloud_secretmanager_v1::model::secret_version::State;
        let enabled = State::Enabled;
        println!("State::Enabled = {enabled}");
        assert_eq!(enabled.value(), Some(1));
        assert_eq!(enabled.name(), Some("ENABLED"));

        let state = State::from(1);
        println!("state = {state}");
        assert_eq!(state.value(), Some(1));
        assert_eq!(state.name(), Some("ENABLED"));

        let state = State::from("ENABLED");
        println!("state = {state}");
        assert_eq!(state.value(), Some(1));
        assert_eq!(state.name(), Some("ENABLED"));
        println!("json = {}", serde_json::to_value(&state)?);

Handling unknown values

When using unknown string values, the .value() function return None, but everything else works as normal:

        use google_cloud_secretmanager_v1::model::secret_version::State;
        use serde_json::json;
        let state = State::from("STATE_NAME_FROM_THE_FUTURE");
        println!("state = {state}");
        assert_eq!(state.value(), None);
        assert_eq!(state.name(), Some("STATE_NAME_FROM_THE_FUTURE"));
        println!("json = {}", serde_json::to_value(&state)?);
        let u = serde_json::from_value::<State>(json!("STATE_NAME_FROM_THE_FUTURE"))?;
        assert_eq!(state, u);

The same principle applies to unknown integer values:

        use google_cloud_secretmanager_v1::model::secret_version::State;
        use serde_json::json;
        let state = State::from("STATE_NAME_FROM_THE_FUTURE");
        println!("state = {state}");
        assert_eq!(state.value(), None);
        assert_eq!(state.name(), Some("STATE_NAME_FROM_THE_FUTURE"));
        println!("json = {}", serde_json::to_value(&state)?);
        let u = serde_json::from_value::<State>(json!("STATE_NAME_FROM_THE_FUTURE"))?;
        assert_eq!(state, u);

Preparing for upgrades

As mentioned above, the Rust enumerations in the client libraries may gain new variants in future releases. To avoid breaking applications we mark these enumerations as #[non_exhaustive].

If you use a match expression for non-exhaustive enumerations then you must include the wildcard pattern in your match. This will prevent compilation problems when new variants are included in the enumeration.

    use google_cloud_secretmanager_v1::model::secret_version::State;
    fn match_with_wildcard(state: State) -> anyhow::Result<()> {
        use anyhow::Error;
        match state {
            State::Unspecified => {
                return Err(Error::msg("the documentation says this is never used"));
            }
            State::Enabled => println!("the secret is enabled and can be accessed"),
            State::Disabled => {
                println!("the secret version is not accessible until it is enabled")
            }
            State::Destroyed => {
                println!("the secret is destroyed, the data is no longer accessible")
            }
            State::UnknownValue(u) => {
                println!("unknown State variant ({u:?}) time to update the library")
            }
            _ => return Err(Error::msg("unexpected value, update this code")),
        };
        Ok(())
    }

Nevertheless, you may want a warning or error if new variants appear, at least so you can examine the code and decide if it must be updated. If that is the case, consider using the wildcard_enum_match_arm clippy warning:

    use google_cloud_secretmanager_v1::model::secret_version::State;
    fn match_with_warnings(state: State) -> anyhow::Result<()> {
        use anyhow::Error;
        #[warn(clippy::wildcard_enum_match_arm)]
        match state {
            State::Unspecified => {
                return Err(Error::msg("the documentation says this is never used"));
            }
            State::Enabled => println!("the secret is enabled and can be accessed"),
            State::Disabled => {
                println!("the secret version is not accessible until it is enabled")
            }
            State::Destroyed => {
                println!("the secret is destroyed, the data is no longer accessible")
            }
            State::UnknownValue(u) => {
                println!("unknown State variant ({u:?}) time to update the library")
            }
            _ => {
                // *If* your CI includes treating clippy warnings as errors,
                // consider using `unreachable!()`.
                return Err(Error::msg("unexpected value, update this code"));
            }
        };
        Ok(())
    }

You may also consider the (currently unstable) non_exhaustive_omitted_patterns lint.