Distributed counters

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

Note: This product is not available on watchOS and App Clip targets.
// counters/${ID}structCounter{letnumShards:Intinit(numShards:Int){self.numShards=numShards}}// counters/${ID}/shards/${NUM}structShard{letcount:Intinit(count:Int){self.count=count}}

Objective-C

Note: This product is not available on watchOS and App Clip targets.
// 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_shards

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_shards

Node.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

Note: This product is not available on watchOS and App Clip targets.
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

Note: This product is not available on watchOS and App Clip targets.
-(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

Note: This product is not available on watchOS and App Clip targets.
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

Note: This product is not available on watchOS and App Clip targets.
-(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

Note: This product is not available on watchOS and App Clip targets.
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

Note: This product is not available on watchOS and App Clip targets.
-(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)returntotal

Python

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)returntotal

Node.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.