Bulk delete
Firestore’s BulkDeleteDocuments operation removes every document in one or more collection
groups in the background, without you reading or writing each document yourself. The library
exposes it through db.fluent().delete().bulk(), behind the same admin feature index
management uses; see Enabling the admin
feature for turning it on. Google’s own
documentation for the underlying operation is at
https://cloud.google.com/firestore/docs/manage-data/bulk-delete.
Why the builder takes collection groups
.bulk() takes .collection_groups(...), and there is no .parent(): Google’s
BulkDeleteDocuments deletes every document in every collection with the given ID, at any depth,
across the whole database, and Firestore offers no parent-scoped bulk delete. Naming a collection
group called orders deletes every orders collection under every document in the database,
including ones your code never queries.
What survives a bulk delete
- documents written after the operation starts processing;
- any collection whose ID does not match a named group, even nested under a deleted one.
Background and non-transactional
Firestore picks the documents to delete as of one snapshot time, which the result reports as
snapshot_time. The deletes themselves are not one transaction, so other writes can interleave
with them while the operation runs. execute() returns once the
operation is requested unless you ask it to wait; either way the deletion itself continues on
Firestore’s side, independent of your process.
Declaring a bulk delete
use firestore::*;
async fn example(db: FirestoreDb) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = db
.fluent()
.delete()
.bulk()
.collection_groups(["orders"])
.execute()
.await?;
println!("{result}");
Ok(())
}
.collection_groups(...) replaces whatever an earlier call in the same chain set, so call it
once with every group you want removed.
The empty-list and duplicate-group rules
An empty list is rejected before anything is sent: Firestore reads an empty collection_ids as
“the whole database”, and the library refuses to send that on your behalf. Naming the same group
twice is rejected too. Both checks run at .execute(), alongside FirestoreCollectionId’s own
validation of each name, and return InvalidParametersError naming what is wrong, without making
a request.
Waiting for the operation and reading the result
By default execute() returns as soon as Firestore accepts the operation. Add
.wait_until_done(timeout) to poll until it reaches a terminal state instead, or
.wait_until_done_with_options(...) for control over the poll interval too:
use firestore::*;
use std::time::Duration;
async fn example(db: FirestoreDb) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let result = db
.fluent()
.delete()
.bulk()
.collection_groups(["orders"])
.wait_until_done(Duration::from_secs(300))
.execute()
.await?;
println!("{result}");
Ok(())
}
The returned FirestoreBulkDeleteResult carries the operation name, the collection groups you
asked for, and the elapsed time, plus, once you wait for it, the snapshot time Firestore used to
decide what to delete and document and byte progress; without .wait_until_done(...), the
snapshot time and progress stay unset and print as “progress not reported”, never as 0/0. It
implements Display, so printing or logging it directly works with or without a tracing
subscriber.
The timeout works the same way as for index management’s .wait_until_ready(...): the last poll
goes out at the deadline and may take up to 10 seconds to answer, or the whole timeout if that is
shorter.
A failed wait leaves the operation running on Firestore’s side; the library logs the result built
from what was polled so far at warn before returning the error, the same as index management’s
.sync() does for its own wait.
Logs and spans
A bulk delete logs through tracing, one span covering the whole call (Firestore Bulk Delete),
nesting a Firestore Bulk Delete Wait child span when you wait. An excerpt of the log:
Started a bulk delete. operation=".../operations/CyA0..." action="bulk_delete" collection_groups="firestore-rs-bulk-delete-test"
Firestore reported the bulk delete's snapshot time. snapshot_time=2026-09-26T19:50:00Z
Operation reached a terminal state. action="bulk_delete" polls=3
The third line comes from the same wait every long-running admin operation polls through, index sync included, which is why it names the operation generically rather than as a bulk delete.
A bulk delete is an operation that Firestore runs in the background, and it takes time even for a few documents, from about ten seconds to about a minute. The time grows with the data, so set the wait timeout well above that.
Bulk delete works on both editions, and removes the named collection group at every depth.
The Firestore emulator
The emulator answers BulkDeleteDocuments with UNIMPLEMENTED, which a bulk delete returns as
DatabaseError.
IAM roles
roles/datastore.bulkAdmin carries datastore.databases.bulkDelete, the permission a bulk delete
needs. roles/datastore.owner is a superset and also works.
Running the tests against a real project
tests/admin-bulk-delete-tests.rs writes and bulk-deletes real documents in GCP_PROJECT, so it
carries #[ignore] and only runs explicitly:
GCP_PROJECT=your-project cargo test --test admin-bulk-delete-tests --features admin -- --ignored --nocapture
Full example available here.