Firestore transactions require an active network connection to the backend and will fail immediately when the client is offline. This is by design—transactions need to read and verify data on the server to ensure consistency.
Firestore transactions are designed to perform atomic operations on one or more documents. They require real-time communication with the Firestore backend to: 1. Read the current state of documents involved in the transaction 2. Execute the transaction handler function (which may run multiple times due to optimistic concurrency control) 3. Verify changes haven't conflicted with other concurrent writes 4. Commit the changes atomically When your client is offline, the SDK cannot communicate with Firestore servers, so transactions fail immediately. Unlike regular writes (set, update, delete) which queue offline and sync when reconnected, transactions cannot be queued because they depend on server-side validation and consistency guarantees.
Use the enableNetwork() and disableNetwork() methods to detect and handle offline state:
// JavaScript/Web
import { collection, query, onSnapshot, runTransaction, disableNetwork, enableNetwork } from "firebase/firestore";
const db = getFirestore();
// Check if currently online
firebase.firestore().disableNetwork().then(() => {
// Network is disabled (offline mode)
}).catch(() => {
// Network is enabled (online mode)
});// Android
import com.google.firebase.firestore.ktx.firestore
import kotlinx.coroutines.*
val db = Firebase.firestore
db.disableNetwork()
.addOnSuccessListener {
// Offline mode enabled
}
.addOnFailureListener { e ->
// Network is already disabled or error occurred
}Wrap transaction calls in a check:
if (navigator.onLine) {
// Safe to run transaction
await db.runTransaction(async (transaction) => {
// Your transaction code
});
} else {
console.warn("Cannot run transaction: client is offline");
// Handle offline case
}If you need atomic operations that work offline, use batch writes instead. Batch writes queue offline and sync when the connection is restored:
// This works offline and queues until reconnection
const batch = db.batch();
const ref1 = db.collection("users").doc("user1");
batch.set(ref1, { name: "Alice", updated: new Date() });
const ref2 = db.collection("users").doc("user2");
batch.update(ref2, { status: "active" });
const ref3 = db.collection("logs").doc("log1");
batch.delete(ref3);
await batch.commit(); // Will queue if offlineImportant limitation: Batch writes cannot read data, only write. They cannot enforce consistency like transactions do. Use them when you don't need to read existing data to decide what to write.
The best long-term solution is to minimize transaction usage by structuring your data to support offline-first operations:
1. Denormalize data: Store copies of needed data to avoid cross-document reads
// Instead of: transaction reads user.profileId, then updates profile
// Store: user document includes denormalized profile fields
{
userId: "123",
profileName: "Alice", // Denormalized copy
profilePhoto: "url" // Denormalized copy
}2. Use document IDs strategically: Generate IDs client-side (UUID) so you can write without reading
// Generate ID first
const postId = uuidv4();
// Write without reading
await db.collection("posts").doc(postId).set({
id: postId,
title: "My post",
authorId: currentUser.uid,
timestamp: serverTimestamp()
});3. Separate concerns: Use different documents for data that doesn't need to be atomic together
Always wrap transactions in try-catch to gracefully handle offline scenarios:
try {
await db.runTransaction(async (transaction) => {
const userRef = db.collection("users").doc("user123");
const userData = await transaction.get(userRef);
if (!userData.exists) {
throw new Error("User not found");
}
const newBalance = userData.data().balance - 100;
transaction.update(userRef, { balance: newBalance });
});
console.log("Transaction succeeded");
} catch (error) {
if (error.code === "UNAVAILABLE") {
console.error("Transaction failed: client is offline or server unavailable");
// Show user-friendly message
// Retry when connection restored
} else if (error.code === "FAILED_PRECONDITION") {
console.error("Transaction precondition failed");
} else {
console.error("Transaction failed:", error);
}
}// Android Kotlin
try {
db.runTransaction { transaction ->
val userRef = db.collection("users").document("user123")
val userData = transaction.get(userRef)
val balance = userData.getLong("balance") ?: 0L
transaction.update(userRef, "balance", balance - 100)
}.addOnSuccessListener {
Log.d(TAG, "Transaction succeeded")
}.addOnFailureListener { e ->
when {
e.code == FirebaseFirestoreException.Code.UNAVAILABLE -> {
Log.e(TAG, "Offline or server unavailable")
}
else -> Log.e(TAG, "Transaction failed", e)
}
}
} catch (e: Exception) {
Log.e(TAG, "Error", e)
}On Android and iOS, offline persistence is enabled by default. On the web, you must enable it explicitly:
// Web - Enable offline persistence
import { initializeFirestore, enableIndexedDbPersistence } from "firebase/firestore";
const db = initializeFirestore(app, {
// config options
});
enableIndexedDbPersistence(db)
.catch((err) => {
if (err.code == "failed-precondition") {
console.error("Multiple tabs open - persistence disabled");
} else if (err.code == "unimplemented") {
console.error("Browser doesn't support persistence");
}
});Offline persistence allows reads from the local cache when offline, but transactions still cannot run. Use it with batch writes for the best offline experience.
Transaction Timeouts and Limits: Even when online, transactions have strict constraints:
- Transaction handler must complete within 270 seconds total
- Idle timeout: if no activity for 60 seconds, transaction fails
- Lock deadline: 20 seconds per read—if a lock cannot be acquired in 20 seconds, the transaction fails and is retried
Optimistic Locking Retry Behavior: Firestore uses optimistic concurrency control. If the transaction handler reads data that has been modified by another client, Firestore automatically retries the entire transaction handler. This retry loop requires active server communication, making offline transactions impossible.
Different Offline Scenarios:
- No network connection: Transaction fails immediately with "UNAVAILABLE" error
- High latency/poor connection: Transaction may timeout if it cannot complete the read-verify-write cycle quickly enough
- Server busy: May fail with transient errors requiring exponential backoff retry logic
Callable Functions: INTERNAL - Unhandled exception
How to fix "Callable Functions: INTERNAL - Unhandled exception" in Firebase
auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options
How to fix "auth/invalid-hash-algorithm: Hash algorithm doesn't match supported options" in Firebase
Hosting: CORS configuration not set up properly
How to fix CORS configuration in Firebase Hosting
auth/reserved-claims: Custom claims use reserved OIDC claim names
How to fix "reserved claims" error when setting custom claims in Firebase
Callable Functions: UNAUTHENTICATED - Invalid credentials
How to fix "UNAUTHENTICATED - Invalid credentials" in Firebase Callable Functions