Cloud Firestore supports atomic operations for reading
and writing data. In a set of atomic operations, either all of the operations
succeed, or none of them are applied. There are two types of atomic operations
in Cloud Firestore:
- Transactions
: a
transaction
is a set of
read and write operations on one or more documents.
- Batched Writes
: a
batched write
is a set of
write operations on one or more documents.
Updating data with transactions
Using the Cloud Firestore client libraries, you can group multiple
operations into a single transaction. Transactions are useful when you want to
update a field's value based on its current value, or the value of some other
field.
A transaction consists of any number of
get()
operations followed by any number of write operations such as
set()
,
update()
, or
delete()
. In the case of a concurrent edit,
Cloud Firestore runs the entire transaction again. For example, if a
transaction reads documents and another client modifies any of those documents,
Cloud Firestore retries the transaction. This feature ensures that the
transaction runs on up-to-date and consistent data.
Transactions never partially
apply writes. All writes execute at the end of a successful transaction.
When using transactions, note that:
- Read operations must come before write operations.
- A function calling a transaction (transaction function) might run
more than once if a concurrent edit affects a document that the transaction
reads.
- Transaction functions should not directly modify application state.
- Transactions will fail when the client is offline.
The following example shows how to create and run a transaction:
Web modular API
import { runTransaction } from "firebase/firestore";
try {
await runTransaction(db, async (transaction) => {
const sfDoc = await transaction.get(sfDocRef);
if (!sfDoc.exists()) {
throw "Document does not exist!";
}
const newPopulation = sfDoc.data().population + 1;
transaction.update(sfDocRef, { population: newPopulation });
});
console.log("Transaction successfully committed!");
} catch (e) {
console.log("Transaction failed: ", e);
}
Web namespaced API
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
// Uncomment to initialize the doc.
// sfDocRef.set({ population: 0 });
return db.runTransaction((transaction) => {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(sfDocRef).then((sfDoc) => {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
// Add one person to the city population.
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
var newPopulation = sfDoc.data().population + 1;
transaction.update(sfDocRef, { population: newPopulation });
});
}).then(() => {
console.log("Transaction successfully committed!");
}).catch((error) => {
console.log("Transaction failed: ", error);
});
Swift
Note:
This product is not available on watchOS and App Clip targets.
let sfReference = db.collection("cities").document("SF")
do {
let _ = try await db.runTransaction({ (transaction, errorPointer) -> Any? in
let sfDocument: DocumentSnapshot
do {
try sfDocument = transaction.getDocument(sfReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
transaction.updateData(["population": oldPopulation + 1], forDocument: sfReference)
return nil
})
print("Transaction successfully committed!")
} catch {
print("Transaction failed: \(error)")
}
Objective-C
Note:
This product is not available on watchOS and App Clip targets.
FIRDocumentReference *sfReference =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[self.db runTransactionWithBlock:^id (FIRTransaction *transaction, NSError **errorPointer) {
FIRDocumentSnapshot *sfDocument = [transaction getDocument:sfReference error:errorPointer];
if (*errorPointer != nil) { return nil; }
if (![sfDocument.data[@"population"] isKindOfClass:[NSNumber class]]) {
*errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-1 userInfo:@{
NSLocalizedDescriptionKey: @"Unable to retreive population from snapshot"
}];
return nil;
}
NSInteger oldPopulation = [sfDocument.data[@"population"] integerValue];
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
[transaction updateData:@{ @"population": @(oldPopulation + 1) } forDocument:sfReference];
return nil;
} completion:^(id result, NSError *error) {
if (error != nil) {
NSLog(@"Transaction failed: %@", error);
} else {
NSLog(@"Transaction successfully committed!");
}
}];
Kotlin+KTX
val sfDocRef = db.collection("cities").document("SF")
db.runTransaction { transaction ->
val snapshot = transaction.get(sfDocRef)
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
val newPopulation = snapshot.getDouble("population")!! + 1
transaction.update(sfDocRef, "population", newPopulation)
// Success
null
}.addOnSuccessListener { Log.d(TAG, "Transaction success!") }
.addOnFailureListener { e -> Log.w(TAG, "Transaction failure.", e) }
Java
final DocumentReference sfDocRef = db.collection("cities").document("SF");
db.runTransaction(new Transaction.Function<Void>() {
@Override
public Void apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction.get(sfDocRef);
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
double newPopulation = snapshot.getDouble("population") + 1;
transaction.update(sfDocRef, "population", newPopulation);
// Success
return null;
}
}).addOnSuccessListener(new OnSuccessListener<Void>() {
@Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "Transaction success!");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "Transaction failure.", e);
}
});
Dart
final sfDocRef = db.collection("cities").doc("SF");
db.runTransaction((transaction) async {
final snapshot = await transaction.get(sfDocRef);
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
final newPopulation = snapshot.get("population") + 1;
transaction.update(sfDocRef, {"population": newPopulation});
}).then(
(value) => print("DocumentSnapshot successfully updated!"),
onError: (e) => print("Error updating document $e"),
);
C++
DocumentReference sf_doc_ref = db->Collection("cities").Document("SF");
db->RunTransaction([sf_doc_ref](Transaction& transaction,
std::string& out_error_message) -> Error {
Error error = Error::kErrorOk;
DocumentSnapshot snapshot =
transaction.Get(sf_doc_ref, &error, &out_error_message);
// Note: this could be done without a transaction by updating the
// population using FieldValue::Increment().
std::int64_t new_population =
snapshot.Get("population").integer_value() + 1;
transaction.Update(
sf_doc_ref,
{{"population", FieldValue::Integer(new_population)}});
return Error::kErrorOk;
}).OnCompletion([](const Future<void>& future) {
if (future.error() == Error::kErrorOk) {
std::cout << "Transaction success!" << std::endl;
} else {
std::cout << "Transaction failure: " << future.error_message() << std::endl;
}
});
Unity
DocumentReference cityRef = db.Collection("cities").Document("SF");
db.RunTransactionAsync(transaction =>
{
return transaction.GetSnapshotAsync(cityRef).ContinueWith((snapshotTask) =>
{
DocumentSnapshot snapshot = snapshotTask.Result;
long newPopulation = snapshot.GetValue<long>("Population") + 1;
Dictionary<string, object> updates = new Dictionary<string, object>
{
{ "Population", newPopulation}
};
transaction.Update(cityRef, updates);
});
});
Do not modify application state inside of your transaction functions. Doing so
will introduce concurrency issues, because transaction functions can run
multiple times and are not guaranteed to run on the UI thread. Instead, pass
information you need out of your transaction functions. The following example
builds on the previous example to show how to pass information out of a
transaction:
Web modular API
import { doc, runTransaction } from "firebase/firestore";
// Create a reference to the SF doc.
const sfDocRef = doc(db, "cities", "SF");
try {
const newPopulation = await runTransaction(db, async (transaction) => {
const sfDoc = await transaction.get(sfDocRef);
if (!sfDoc.exists()) {
throw "Document does not exist!";
}
const newPop = sfDoc.data().population + 1;
if (newPop <= 1000000) {
transaction.update(sfDocRef, { population: newPop });
return newPop;
} else {
return Promise.reject("Sorry! Population is too big");
}
});
console.log("Population increased to ", newPopulation);
} catch (e) {
// This will be a "population is too big" error.
console.error(e);
}
Web namespaced API
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
db.runTransaction((transaction) => {
return transaction.get(sfDocRef).then((sfDoc) => {
if (!sfDoc.exists) {
throw "Document does not exist!";
}
var newPopulation = sfDoc.data().population + 1;
if (newPopulation <= 1000000) {
transaction.update(sfDocRef, { population: newPopulation });
return newPopulation;
} else {
return Promise.reject("Sorry! Population is too big.");
}
});
}).then((newPopulation) => {
console.log("Population increased to ", newPopulation);
}).catch((err) => {
// This will be an "population is too big" error.
console.error(err);
});
Swift
Note:
This product is not available on watchOS and App Clip targets.
let sfReference = db.collection("cities").document("SF")
do {
let object = try await db.runTransaction({ (transaction, errorPointer) -> Any? in
let sfDocument: DocumentSnapshot
do {
try sfDocument = transaction.getDocument(sfReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let oldPopulation = sfDocument.data()?["population"] as? Int else {
let error = NSError(
domain: "AppErrorDomain",
code: -1,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve population from snapshot \(sfDocument)"
]
)
errorPointer?.pointee = error
return nil
}
// Note: this could be done without a transaction
// by updating the population using FieldValue.increment()
let newPopulation = oldPopulation + 1
guard newPopulation <= 1000000 else {
let error = NSError(
domain: "AppErrorDomain",
code: -2,
userInfo: [NSLocalizedDescriptionKey: "Population \(newPopulation) too big"]
)
errorPointer?.pointee = error
return nil
}
transaction.updateData(["population": newPopulation], forDocument: sfReference)
return newPopulation
})
print("Population increased to \(object!)")
} catch {
print("Error updating population: \(error)")
}
Objective-C
Note:
This product is not available on watchOS and App Clip targets.
FIRDocumentReference *sfReference =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[self.db runTransactionWithBlock:^id (FIRTransaction *transaction, NSError **errorPointer) {
FIRDocumentSnapshot *sfDocument = [transaction getDocument:sfReference error:errorPointer];
if (*errorPointer != nil) { return nil; }
if (![sfDocument.data[@"population"] isKindOfClass:[NSNumber class]]) {
*errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-1 userInfo:@{
NSLocalizedDescriptionKey: @"Unable to retreive population from snapshot"
}];
return nil;
}
NSInteger population = [sfDocument.data[@"population"] integerValue];
population++;
if (population >= 1000000) {
*errorPointer = [NSError errorWithDomain:@"AppErrorDomain" code:-2 userInfo:@{
NSLocalizedDescriptionKey: @"Population too big"
}];
return @(population);
}
[transaction updateData:@{ @"population": @(population) } forDocument:sfReference];
return nil;
} completion:^(id result, NSError *error) {
if (error != nil) {
NSLog(@"Transaction failed: %@", error);
} else {
NSLog(@"Population increased to %@", result);
}
}];
Kotlin+KTX
val sfDocRef = db.collection("cities").document("SF")
db.runTransaction { transaction ->
val snapshot = transaction.get(sfDocRef)
val newPopulation = snapshot.getDouble("population")!! + 1
if (newPopulation <= 1000000) {
transaction.update(sfDocRef, "population", newPopulation)
newPopulation
} else {
throw FirebaseFirestoreException(
"Population too high",
FirebaseFirestoreException.Code.ABORTED,
)
}
}.addOnSuccessListener { result ->
Log.d(TAG, "Transaction success: $result")
}.addOnFailureListener { e ->
Log.w(TAG, "Transaction failure.", e)
}
Java
final DocumentReference sfDocRef = db.collection("cities").document("SF");
db.runTransaction(new Transaction.Function<Double>() {
@Override
public Double apply(@NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot snapshot = transaction.get(sfDocRef);
double newPopulation = snapshot.getDouble("population") + 1;
if (newPopulation <= 1000000) {
transaction.update(sfDocRef, "population", newPopulation);
return newPopulation;
} else {
throw new FirebaseFirestoreException("Population too high",
FirebaseFirestoreException.Code.ABORTED);
}
}
}).addOnSuccessListener(new OnSuccessListener<Double>() {
@Override
public void onSuccess(Double result) {
Log.d(TAG, "Transaction success: " + result);
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Log.w(TAG, "Transaction failure.", e);
}
});
Dart
final sfDocRef = db.collection("cities").doc("SF");
db.runTransaction((transaction) {
return transaction.get(sfDocRef).then((sfDoc) {
final newPopulation = sfDoc.get("population") + 1;
transaction.update(sfDocRef, {"population": newPopulation});
return newPopulation;
});
}).then(
(newPopulation) => print("Population increased to $newPopulation"),
onError: (e) => print("Error updating document $e"),
);
C++
// This is not yet supported.
Unity
DocumentReference cityRef = db.Collection("cities").Document("SF");
db.RunTransactionAsync(transaction =>
{
return transaction.GetSnapshotAsync(cityRef).ContinueWith((task) =>
{
long newPopulation = task.Result.GetValue<long>("Population") + 1;
if (newPopulation <= 1000000)
{
Dictionary<string, object> updates = new Dictionary<string, object>
{
{ "Population", newPopulation}
};
transaction.Update(cityRef, updates);
return true;
}
else
{
return false;
}
});
}).ContinueWith((transactionResultTask) =>
{
if (transactionResultTask.Result)
{
Console.WriteLine("Population updated successfully.");
}
else
{
Console.WriteLine("Sorry! Population is too big.");
}
});
Transaction failure
A transaction can fail for the following reasons:
- The transaction contains read operations after write operations.
Read operations must always come before any write operations.
- The transaction read a document that was modified outside of the transaction.
In this case, the transaction automatically runs again. The
transaction is retried a finite number of times.
The transaction exceeded the maximum request size of 10 MiB.
Transaction size depends on the sizes of documents and index entries
modified by the transaction. For a delete operation, this includes the size
of the target document and the sizes of the index entries deleted in
response to the operation.
A failed transaction returns an error and does not write anything to the
database. You do not need to roll back the transaction; Cloud Firestore
does this automatically.
Batched writes
If you do not need to read any documents in your operation set, you can execute
multiple write operations as a single batch that contains any combination of
set()
,
update()
, or
delete()
operations. A batch of writes completes
atomically and can write to multiple documents. The following
example shows how to build and commit a write batch:
Web modular API
import { writeBatch, doc } from "firebase/firestore";
// Get a new write batch
const batch = writeBatch(db);
// Set the value of 'NYC'
const nycRef = doc(db, "cities", "NYC");
batch.set(nycRef, {name: "New York City"});
// Update the population of 'SF'
const sfRef = doc(db, "cities", "SF");
batch.update(sfRef, {"population": 1000000});
// Delete the city 'LA'
const laRef = doc(db, "cities", "LA");
batch.delete(laRef);
// Commit the batch
await batch.commit();
Web namespaced API
// Get a new write batch
var batch = db.batch();
// Set the value of 'NYC'
var nycRef = db.collection("cities").doc("NYC");
batch.set(nycRef, {name: "New York City"});
// Update the population of 'SF'
var sfRef = db.collection("cities").doc("SF");
batch.update(sfRef, {"population": 1000000});
// Delete the city 'LA'
var laRef = db.collection("cities").doc("LA");
batch.delete(laRef);
// Commit the batch
batch.commit().then(() => {
// ...
});
Swift
Note:
This product is not available on watchOS and App Clip targets.
// Get new write batch
let batch = db.batch()
// Set the value of 'NYC'
let nycRef = db.collection("cities").document("NYC")
batch.setData([:], forDocument: nycRef)
// Update the population of 'SF'
let sfRef = db.collection("cities").document("SF")
batch.updateData(["population": 1000000 ], forDocument: sfRef)
// Delete the city 'LA'
let laRef = db.collection("cities").document("LA")
batch.deleteDocument(laRef)
// Commit the batch
do {
try await batch.commit()
print("Batch write succeeded.")
} catch {
print("Error writing batch: \(error)")
}
Objective-C
Note:
This product is not available on watchOS and App Clip targets.
// Get new write batch
FIRWriteBatch *batch = [self.db batch];
// Set the value of 'NYC'
FIRDocumentReference *nycRef =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"NYC"];
[batch setData:@{} forDocument:nycRef];
// Update the population of 'SF'
FIRDocumentReference *sfRef =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"SF"];
[batch updateData:@{ @"population": @1000000 } forDocument:sfRef];
// Delete the city 'LA'
FIRDocumentReference *laRef =
[[self.db collectionWithPath:@"cities"] documentWithPath:@"LA"];
[batch deleteDocument:laRef];
// Commit the batch
[batch commitWithCompletion:^(NSError * _Nullable error) {
if (error != nil) {
NSLog(@"Error writing batch %@", error);
} else {
NSLog(@"Batch write succeeded.");
}
}];
Kotlin+KTX
val nycRef = db.collection("cities").document("NYC")
val sfRef = db.collection("cities").document("SF")
val laRef = db.collection("cities").document("LA")
// Get a new write batch and commit all write operations
db.runBatch { batch ->
// Set the value of 'NYC'
batch.set(nycRef, City())
// Update the population of 'SF'
batch.update(sfRef, "population", 1000000L)
// Delete the city 'LA'
batch.delete(laRef)
}.addOnCompleteListener {
// ...
}
Java
// Get a new write batch
WriteBatch batch = db.batch();
// Set the value of 'NYC'
DocumentReference nycRef = db.collection("cities").document("NYC");
batch.set(nycRef, new City());
// Update the population of 'SF'
DocumentReference sfRef = db.collection("cities").document("SF");
batch.update(sfRef, "population", 1000000L);
// Delete the city 'LA'
DocumentReference laRef = db.collection("cities").document("LA");
batch.delete(laRef);
// Commit the batch
batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
// ...
}
});
Dart
// Get a new write batch
final batch = db.batch();
// Set the value of 'NYC'
var nycRef = db.collection("cities").doc("NYC");
batch.set(nycRef, {"name": "New York City"});
// Update the population of 'SF'
var sfRef = db.collection("cities").doc("SF");
batch.update(sfRef, {"population": 1000000});
// Delete the city 'LA'
var laRef = db.collection("cities").doc("LA");
batch.delete(laRef);
// Commit the batch
batch.commit().then((_) {
// ...
});
C++
// Get a new write batch
WriteBatch batch = db->batch();
// Set the value of 'NYC'
DocumentReference nyc_ref = db->Collection("cities").Document("NYC");
batch.Set(nyc_ref, {});
// Update the population of 'SF'
DocumentReference sf_ref = db->Collection("cities").Document("SF");
batch.Update(sf_ref, {{"population", FieldValue::Integer(1000000)}});
// Delete the city 'LA'
DocumentReference la_ref = db->Collection("cities").Document("LA");
batch.Delete(la_ref);
// Commit the batch
batch.Commit().OnCompletion([](const Future<void>& future) {
if (future.error() == Error::kErrorOk) {
std::cout << "Write batch success!" << std::endl;
} else {
std::cout << "Write batch failure: " << future.error_message() << std::endl;
}
});
Unity
WriteBatch batch = db.StartBatch();
// Set the data for NYC
DocumentReference nycRef = db.Collection("cities").Document("NYC");
Dictionary<string, object> nycData = new Dictionary<string, object>
{
{ "name", "New York City" }
};
batch.Set(nycRef, nycData);
// Update the population for SF
DocumentReference sfRef = db.Collection("cities").Document("SF");
Dictionary<string, object> updates = new Dictionary<string, object>
{
{ "Population", 1000000}
};
batch.Update(sfRef, updates);
// Delete LA
DocumentReference laRef = db.Collection("cities").Document("LA");
batch.Delete(laRef);
// Commit the batch
batch.CommitAsync();
A batched write can contain up to 500 operations. Each operation in the batch
counts separately towards your Cloud Firestore usage.
Like transactions, batched writes are atomic. Unlike transactions, batched
writes do not need to ensure that read documents remain un-modified which leads
to fewer failure cases. They are not subject to retries or
to failures from too many retries. Batched writes execute even when the
user's device is offline.
Data validation for atomic operations
For mobile/web client libraries, you can validate data using
Cloud Firestore Security Rules
. You can ensure that related documents are
always updated atomically and always as part of a transaction or batched write.
Use the
getAfter()
security rule function to access and validate
the state of a document after a set of operations completes but
before
Cloud Firestore commits the operations.
For example, imagine that the database for the
cities
example also contains a
countries
collection. Each
country
document uses a
last_updated
field to
keep track of the last time any city related to that country was updated. The
following security rules require that an update to a
city
document must also
atomically update the related country's
last_updated
field:
service cloud.firestore {
match /databases/{database}/documents {
// If you update a city doc, you must also
// update the related country's last_updated field.
match /cities/{city} {
allow write: if request.auth != null &&
getAfter(
/databases/$(database)/documents/countries/$(request.resource.data.country)
).data.last_updated == request.time;
}
match /countries/{country} {
allow write: if request.auth != null;
}
}
}
Security rules limits
In security rules for transactions or batched writes, there is a
limit
of 20 document access calls for the entire atomic operation in addition to the
normal 10 call limit for each single document operation in the batch.
For example, consider the following rules for a chat application:
service cloud.firestore {
match /databases/{db}/documents {
function prefix() {
return /databases/{db}/documents;
}
match /chatroom/{roomId} {
allow read, write: if request.auth != null && roomId in get(/$(prefix())/users/$(request.auth.uid)).data.chats
|| exists(/$(prefix())/admins/$(request.auth.uid));
}
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId
|| exists(/$(prefix())/admins/$(request.auth.uid));
}
match /admins/{userId} {
allow read, write: if request.auth != null && exists(/$(prefix())/admins/$(request.auth.uid));
}
}
}
The snippets below illustrate the number of document access calls used for
a few data access patterns:
// 0 document access calls used, because the rules evaluation short-circuits
// before the exists() call is invoked.
db.collection('user').doc('myuid').get(...);
// 1 document access call used. The maximum total allowed for this call
// is 10, because it is a single document request.
db.collection('chatroom').doc('mygroup').get(...);
// Initializing a write batch...
var batch = db.batch();
// 2 document access calls used, 10 allowed.
var group1Ref = db.collection("chatroom").doc("group1");
batch.set(group1Ref, {msg: "Hello, from Admin!"});
// 1 document access call used, 10 allowed.
var newUserRef = db.collection("users").doc("newuser");
batch.update(newUserRef, {"lastSignedIn": new Date()});
// 1 document access call used, 10 allowed.
var removedAdminRef = db.collection("admin").doc("otheruser");
batch.delete(removedAdminRef);
// The batch used a total of 2 + 1 + 1 = 4 document access calls, out of a total
// 20 allowed.
batch.commit();
For more information on how to resolve latency issues caused by large writes and batched writes, errors due to contention from overlapping transactions, and other issues consider checking out the
troubleshooting page
.