Distributed counters Stay organized with collections Save and categorize content based on your preferences.
Many realtime apps have documents that act as counters. For example, you mightcount 'likes' on a post, or 'favorites' of a specific item.
InCloud Firestore, you can't update a single document at an unlimited rate. If you have a counter based on single document and frequent enough increments to it you will eventually see contention on the updates to the document. SeeUpdates to a single document.
Solution: Distributed counters
To support more frequent counter updates, create a distributed counter.Each counter is a document with a subcollection of "shards," and the value ofthe counter is the sum of the value of the shards.
Write throughput increases linearly with the number of shards, so a distributedcounter with 10 shards can handle 10x as many writes as a traditional counter.
Web
//counters/${ID}{"num_shards":NUM_SHARDS,"shards":[subcollection]}//counters/${ID}/shards/${NUM}{"count":123}Swift
// counters/${ID}structCounter{letnumShards:Intinit(numShards:Int){self.numShards=numShards}}// counters/${ID}/shards/${NUM}structShard{letcount:Intinit(count:Int){self.count=count}}
Objective-C
// counters/${ID}@interfaceFIRCounter :NSObject@property(nonatomic,readonly)NSIntegershardCount;@end@implementationFIRCounter-(instancetype)initWithShardCount:(NSInteger)shardCount{self=[superinit];if(self!=nil){_shardCount=shardCount;}returnself;}@end// counters/${ID}/shards/${NUM}@interfaceFIRShard :NSObject@property(nonatomic,readonly)NSIntegercount;@end@implementationFIRShard-(instancetype)initWithCount:(NSInteger)count{self=[superinit];if(self!=nil){_count=count;}returnself;}@end
Kotlin
// counters/${ID}dataclassCounter(varnumShards:Int)// counters/${ID}/shards/${NUM}dataclassShard(varcount:Int)
Java
// counters/${ID}publicclassCounter{intnumShards;publicCounter(intnumShards){this.numShards=numShards;}}// counters/${ID}/shards/${NUM}publicclassShard{intcount;publicShard(intcount){this.count=count;}}
Python
importrandomfromgoogle.cloudimportfirestoreclassShard:""" A shard is a distributed counter. Each shard can support being incremented once per second. Multiple shards are needed within a Counter to allow more frequent incrementing. """def__init__(self):self._count=0defto_dict(self):return{"count":self._count}classCounter:""" A counter stores a collection of shards which are summed to return a total count. This allows for more frequent incrementing than a single document. """def__init__(self,num_shards):self._num_shards=num_shardsPython
importrandomfromgoogle.cloudimportfirestoreclassShard:""" A shard is a distributed counter. Each shard can support being incremented once per second. Multiple shards are needed within a Counter to allow more frequent incrementing. """def__init__(self):self._count=0defto_dict(self):return{"count":self._count}classCounter:""" A counter stores a collection of shards which are summed to return a total count. This allows for more frequent incrementing than a single document. """def__init__(self,num_shards):self._num_shards=num_shardsNode.js
Not applicable, see the counter increment snippet below.
Go
import("context""fmt""math/rand""strconv""cloud.google.com/go/firestore""google.golang.org/api/iterator")// Counter is a collection of documents (shards)// to realize counter with high frequency.typeCounterstruct{numShardsint}// Shard is a single counter, which is used in a group// of other shards within Counter.typeShardstruct{Countint}PHP
Not applicable, see the counter initialization snippet below.
C#
/// <summary>/// Shard is a document that contains the count./// </summary>[FirestoreData]publicclassShard{[FirestoreProperty(name: "count")]publicintCount{get;set;}}The following code initializes a distributed counter:
Web
functioncreateCounter(ref,num_shards){varbatch=db.batch();// Initialize the counter documentbatch.set(ref,{num_shards:num_shards});// Initialize each shard with count=0for(leti=0;i <num_shards;i++){constshardRef=ref.collection('shards').doc(i.toString());batch.set(shardRef,{count:0});}// Commit the write batchreturnbatch.commit();}
Swift
funccreateCounter(ref:DocumentReference,numShards:Int)async{do{tryawaitref.setData(["numShards":numShards])foriin0...numShards{tryawaitref.collection("shards").document(String(i)).setData(["count":0])}}catch{// ...}}
Objective-C
-(void)createCounterAtReference:(FIRDocumentReference*)referenceshardCount:(NSInteger)shardCount{[referencesetData:@{@"numShards":@(shardCount)}completion:^(NSError*_Nullableerror){for(NSIntegeri=0;i <shardCount;i++){NSString*shardName=[NSStringstringWithFormat:@"%ld",(long)shardCount];[[[referencecollectionWithPath:@"shards"]documentWithPath:shardName]setData:@{@"count":@(0)}];}}];}
Kotlin
funcreateCounter(ref:DocumentReference,numShards:Int):Task<Void>{// Initialize the counter document, then initialize each shard.returnref.set(Counter(numShards)).continueWithTask{task->if(!task.isSuccessful){throwtask.exception!!}valtasks=arrayListOf<Task<Void>>()// Initialize each shard with count=0for(iin0untilnumShards){valmakeShard=ref.collection("shards").document(i.toString()).set(Shard(0))tasks.add(makeShard)}Tasks.whenAll(tasks)}}
Java
publicTask<Void>createCounter(finalDocumentReferenceref,finalintnumShards){// Initialize the counter document, then initialize each shard.returnref.set(newCounter(numShards)).continueWithTask(newContinuation<Void,Task<Void>>(){@OverridepublicTask<Void>then(@NonNullTask<Void>task)throwsException{if(!task.isSuccessful()){throwtask.getException();}List<Task<Void>>tasks=newArrayList<>();// Initialize each shard with count=0for(inti=0;i <numShards;i++){Task<Void>makeShard=ref.collection("shards").document(String.valueOf(i)).set(newShard(0));tasks.add(makeShard);}returnTasks.whenAll(tasks);}});}
Python
definit_counter(self,doc_ref):""" Create a given number of shards as subcollection of specified document. """col_ref=doc_ref.collection("shards")# Initialize each shard with count=0fornuminrange(self._num_shards):shard=Shard()col_ref.document(str(num)).set(shard.to_dict())Python
asyncdefinit_counter(self,doc_ref):""" Create a given number of shards as subcollection of specified document. """col_ref=doc_ref.collection("shards")# Initialize each shard with count=0fornuminrange(self._num_shards):shard=Shard()awaitcol_ref.document(str(num)).set(shard.to_dict())Node.js
Not applicable, see the counter increment snippet below.
Go
// initCounter creates a given number of shards as// subcollection of specified document.func(c*Counter)initCounter(ctxcontext.Context,docRef*firestore.DocumentRef)error{colRef:=docRef.Collection("shards")// Initialize each shard with count=0fornum:=0;num <c.numShards;num++{shard:=Shard{0}if_,err:=colRef.Doc(strconv.Itoa(num)).Set(ctx,shard);err!=nil{returnfmt.Errorf("Set: %w",err)}}returnnil}PHP
$numShards = 10;$ref = $db->collection('samples/php/distributedCounters');for ($i = 0; $i < $numShards; $i++) { $doc = $ref->document((string) $i); $doc->set(['Cnt' => 0]);}C#
/// <summary>/// Create a given number of shards as a/// subcollection of specified document./// </summary>/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>privatestaticasyncTaskCreateCounterAsync(DocumentReferencedocRef,intnumOfShards){CollectionReferencecolRef=docRef.Collection("shards");vartasks=newList<Task>();// Initialize each shard with Count=0for(vari=0;i <numOfShards;i++){tasks.Add(colRef.Document(i.ToString()).SetAsync(newShard(){Count=0}));}awaitTask.WhenAll(tasks);}Ruby
# project_id = "Your Google Cloud Project ID"# num_shards = "Number of shards for distributed counter"# collection_path = "shards"require"google/cloud/firestore"firestore=Google::Cloud::Firestore.newproject_id:project_idshards_ref=firestore.colcollection_path# Initialize each shard with count=0num_shards.timesdo|i|shards_ref.doc(i).set({count:0})endputs"Distributed counter shards collection created."To increment the counter, choose a random shard and incrementthe count:
Web
functionincrementCounter(ref,num_shards){// Select a shard of the counter at randomconstshard_id=Math.floor(Math.random()*num_shards).toString();constshard_ref=ref.collection('shards').doc(shard_id);// Update countreturnshard_ref.update("count",firebase.firestore.FieldValue.increment(1));}
Swift
funcincrementCounter(ref:DocumentReference,numShards:Int){// Select a shard of the counter at randomletshardId=Int(arc4random_uniform(UInt32(numShards)))letshardRef=ref.collection("shards").document(String(shardId))shardRef.updateData(["count":FieldValue.increment(Int64(1))])}
Objective-C
-(void)incrementCounterAtReference:(FIRDocumentReference*)referenceshardCount:(NSInteger)shardCount{// Select a shard of the counter at randomNSIntegershardID=(NSInteger)arc4random_uniform((uint32_t)shardCount);NSString*shardName=[NSStringstringWithFormat:@"%ld",(long)shardID];FIRDocumentReference*shardReference=[[referencecollectionWithPath:@"shards"]documentWithPath:shardName];[shardReferenceupdateData:@{@"count":[FIRFieldValuefieldValueForIntegerIncrement:1]}];}
Kotlin
funincrementCounter(ref:DocumentReference,numShards:Int):Task<Void>{valshardId=Math.floor(Math.random()*numShards).toInt()valshardRef=ref.collection("shards").document(shardId.toString())returnshardRef.update("count",FieldValue.increment(1))}
Java
publicTask<Void>incrementCounter(finalDocumentReferenceref,finalintnumShards){intshardId=(int)Math.floor(Math.random()*numShards);DocumentReferenceshardRef=ref.collection("shards").document(String.valueOf(shardId));returnshardRef.update("count",FieldValue.increment(1));}
Python
defincrement_counter(self,doc_ref):"""Increment a randomly picked shard."""doc_id=random.randint(0,self._num_shards-1)shard_ref=doc_ref.collection("shards").document(str(doc_id))returnshard_ref.update({"count":firestore.Increment(1)})Python
asyncdefincrement_counter(self,doc_ref):"""Increment a randomly picked shard."""doc_id=random.randint(0,self._num_shards-1)shard_ref=doc_ref.collection("shards").document(str(doc_id))returnawaitshard_ref.update({"count":firestore.Increment(1)})Node.js
functionincrementCounter(docRef,numShards){constshardId=Math.floor(Math.random()*numShards);constshardRef=docRef.collection('shards').doc(shardId.toString());returnshardRef.set({count:FieldValue.increment(1)},{merge:true});}Go
// incrementCounter increments a randomly picked shard.func(c*Counter)incrementCounter(ctxcontext.Context,docRef*firestore.DocumentRef)(*firestore.WriteResult,error){docID:=strconv.Itoa(rand.Intn(c.numShards))shardRef:=docRef.Collection("shards").Doc(docID)returnshardRef.Update(ctx,[]firestore.Update{{Path:"Count",Value:firestore.Increment(1)},})}PHP
$ref = $db->collection('samples/php/distributedCounters');$numShards = 0;$docCollection = $ref->documents();foreach ($docCollection as $doc) { $numShards++;}$shardIdx = random_int(0, max(1, $numShards) - 1);$doc = $ref->document((string) $shardIdx);$doc->update([ ['path' => 'Cnt', 'value' => FieldValue::increment(1)]]);C#
/// <summary>/// Increment a randomly picked shard by 1./// </summary>/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>/// <returns>The <see cref="Task"/></returns>privatestaticasyncTaskIncrementCounterAsync(DocumentReferencedocRef,intnumOfShards){intdocumentId;lock(s_randLock){documentId=s_rand.Next(numOfShards);}varshardRef=docRef.Collection("shards").Document(documentId.ToString());awaitshardRef.UpdateAsync("count",FieldValue.Increment(1));}Ruby
# project_id = "Your Google Cloud Project ID"# num_shards = "Number of shards for distributed counter"# collection_path = "shards"require"google/cloud/firestore"firestore=Google::Cloud::Firestore.newproject_id:project_id# Select a shard of the counter at randomshard_id=rand0...num_shardsshard_ref=firestore.doc"#{collection_path}/#{shard_id}"# increment countershard_ref.update({count:firestore.field_increment(1)})puts"Counter incremented."To get the total count, query for all shards and sum theircount fields:
Web
functiongetCount(ref){// Sum the count of each shard in the subcollectionreturnref.collection('shards').get().then((snapshot)=>{lettotal_count=0;snapshot.forEach((doc)=>{total_count+=doc.data().count;});returntotal_count;});}
Swift
funcgetCount(ref:DocumentReference)async{do{letquerySnapshot=tryawaitref.collection("shards").getDocuments()vartotalCount=0fordocumentinquerySnapshot.documents{letcount=document.data()["count"]as!InttotalCount+=count}print("Total count is\(totalCount)")}catch{// handle error}}
Objective-C
-(void)getCountWithReference:(FIRDocumentReference*)reference{[[referencecollectionWithPath:@"shards"]getDocumentsWithCompletion:^(FIRQuerySnapshot*snapshot,NSError*error){NSIntegertotalCount=0;if(error!=nil){// Error getting shards// ...}else{for(FIRDocumentSnapshot*documentinsnapshot.documents){NSIntegercount=[document[@"count"]integerValue];totalCount+=count;}NSLog(@"Total count is %ld",(long)totalCount);}}];}
Kotlin
fungetCount(ref:DocumentReference):Task<Int>{// Sum the count of each shard in the subcollectionreturnref.collection("shards").get().continueWith{task->varcount=0for(snapintask.result!!){valshard=snap.toObject<Shard>()count+=shard.count}count}}
Java
publicTask<Integer>getCount(finalDocumentReferenceref){// Sum the count of each shard in the subcollectionreturnref.collection("shards").get().continueWith(newContinuation<QuerySnapshot,Integer>(){@OverridepublicIntegerthen(@NonNullTask<QuerySnapshot>task)throwsException{intcount=0;for(DocumentSnapshotsnap:task.getResult()){Shardshard=snap.toObject(Shard.class);count+=shard.count;}returncount;}});}
Python
defget_count(self,doc_ref):"""Return a total count across all shards."""total=0shards=doc_ref.collection("shards").list_documents()forshardinshards:total+=shard.get().to_dict().get("count",0)returntotalPython
asyncdefget_count(self,doc_ref):"""Return a total count across all shards."""total=0shards=doc_ref.collection("shards").list_documents()asyncforshardinshards:total+=(awaitshard.get()).to_dict().get("count",0)returntotalNode.js
asyncfunctiongetCount(docRef){constquerySnapshot=awaitdocRef.collection('shards').get();constdocuments=querySnapshot.docs;letcount=0;for(constdocofdocuments){count+=doc.get('count');}returncount;}Go
// getCount returns a total count across all shards.func(c*Counter)getCount(ctxcontext.Context,docRef*firestore.DocumentRef)(int64,error){vartotalint64shards:=docRef.Collection("shards").Documents(ctx)for{doc,err:=shards.Next()iferr==iterator.Done{break}iferr!=nil{return0,fmt.Errorf("Next: %w",err)}vTotal:=doc.Data()["Count"]shardCount,ok:=vTotal.(int64)if!ok{return0,fmt.Errorf("firestore: invalid dataType %T, want int64",vTotal)}total+=shardCount}returntotal,nil}PHP
$result = 0;$docCollection = $db->collection('samples/php/distributedCounters')->documents();foreach ($docCollection as $doc) { $result += $doc->data()['Cnt'];}C#
/// <summary>/// Get total count across all shards./// </summary>/// <param name="docRef">The document reference <see cref="DocumentReference"/></param>/// <returns>The <see cref="int"/></returns>privatestaticasyncTask<int>GetCountAsync(DocumentReferencedocRef){varsnapshotList=awaitdocRef.Collection("shards").GetSnapshotAsync();returnsnapshotList.Sum(shard=>shard.GetValue<int>("count"));}Ruby
# project_id = "Your Google Cloud Project ID"# collection_path = "shards"require"google/cloud/firestore"firestore=Google::Cloud::Firestore.newproject_id:project_idshards_ref=firestore.col_groupcollection_pathcount=0shards_ref.getdo|doc_ref|count+=doc_ref[:count]endputs"Count value is#{count}."Limitations
The solution shown above is a scalable way to create shared counters inCloud Firestore, but you should be aware of the following limitations:
- Shard count - The number of shards controls the performance of thedistributed counter. With too few shards, some transactionsmay have to retry before succeeding, which will slow writes. With too manyshards, reads become slower and more expensive. You can offset theread-expense by keeping the counter total in a separate roll-up documentwhich is updated at a slower cadence, and havingclients read from that document to get the total. The tradeoff is thatclients will have to wait for the roll-up document to be updated, insteadof computing the total by reading all of the shards immediately after anyupdate.
- Cost - The cost of reading a counter value increases linearly with thenumber of shards, because the entire shards subcollection must be loaded.
Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.
Last updated 2026-02-18 UTC.