#How to handle running functions in parallel

9 messages · Page 1 of 1 (latest)

tame heron
#

I have a Mutation, updateViewCount, that is as follows:

export const updateViewCount = mutation({
  args: { id: v.id("intakeForms") },
  handler: async (ctx, { id }) => {
    const intakeForm = await ctx.db.get(id);
    if (!intakeForm) {
      return null;
    }
    await ctx.db.patch(id, { views: (intakeForm.views ?? 0) + 1 });
    const updated = await ctx.db.get(id);
    return updated;
  },
});

This is on a public facing webpage, think of it as a page view counter. This runs without auth check as anyone should be able to access this page and thus make a call to the Mutation. In my Convex Dashboard I get the error: Retried due to write conflicts in table. Which sounds like a race condition. How can I avoid this? How can I add 1 to the field without caring what it's current value is?

stone ingotBOT
#

Hi @tame heron. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:

#

You're encountering write conflicts because your mutation reads and then writes the same document, and when multiple requests happen in parallel (as is common with a public page view counter), Convex's serializable transactions and optimistic concurrency control will retry the mutation until it can complete without conflict. This is expected behavior, but for high-frequency counters, it can lead to performance issues.

Best Practice: Use a Sharded Counter

Convex provides a Sharded Counter component specifically for this use case. Instead of incrementing a single document (which causes contention), the sharded counter distributes increments across multiple documents ("shards"), allowing many increments to happen in parallel with much higher throughput. You can then read the total count by summing across all shards, which is handled for you by the component.

How to use it:

  1. Install the component:

    npm install @convex-dev/sharded-counter
    
  2. Configure the component in your Convex app:

    // convex/convex.config.ts
    import { defineApp } from "convex/server";
    import shardedCounter from "@convex-dev/sharded-counter/convex.config";
    
    const app = defineApp();
    app.use(shardedCounter);
    
    export default app;
    
  3. Use the ShardedCounter in your mutation:

    import { components } from "./_generated/api";
    import { ShardedCounter } from "@convex-dev/sharded-counter";
    
    const viewCounter = new ShardedCounter(components.shardedCounter);
#

export const updateViewCount = mutation({
args: { id: v.id("intakeForms") },
handler: async (ctx, { id }) => {
// Increment the view count for this form
await viewCounter.inc(ctx, id);
// Optionally, return the current count
return await viewCounter.count(ctx, id);
},
});


This approach will eliminate the write conflicts and allow your view counter to scale to high traffic without issues. The sharded counter is atomic, consistent, and designed for exactly this scenario: high-frequency, parallel-safe increments on a public-facing counter [Sharded Counter](<https://www.convex.dev/components/sharded-counter>).

**Why not just patch the document?**
Incrementing a field on a single document is always prone to write conflicts under high concurrency, because every mutation reads and writes the same record. The sharded counter solves this by spreading the load.

If you need to keep the view count in the original document for other reasons, you can periodically sync the sharded counter's value back to the document, but for most analytics and counters, reading from the sharded counter directly is best.

**Summary:**  
Use the Sharded Counter component for high-frequency counters to avoid write conflicts and ensure scalability [Sharded Counter](<https://www.convex.dev/components/sharded-counter>).
----------------------
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
----------------------
#

@tame heron Kapa.ai is still learning and improving, please let me know how I did by reacting below.

tame heron
#

@stone ingot This page is unique per user. The use case is my intake form creation platform. The business creates an intake form specifying fields of what data they want to collect. Then they receive a URL to that form. Using NextJS's slugs, the URL is domain.com/intake/Document_ID, taking the Slug on page load and pulling the form json from Convex which dynamically renders the frontend. On page load, useEffect, it calls the updateViewCount. BEFORE that happens, it checks for another slug domain.com/document_id/unique_id, if unique ID doesn't exist, it genereates one then rewrites the address bar/browser history to have it. Meaning each individual person to open that URL/Scan the QR code WITHOUT the unique ID will then be treated as Unique by the frontend. MEANING It's rare for there to be parallel concurrent updates to that view count field as this use case is for a clinic, doctors, vet etc... where the patient/client can scan a QR Code and fill out the intake form digitally without using pen & paper. This specific Mutation won't occur often as even with a line of 100 Clients, only one person can scan the QR Code at a time and it'd be rare for there to be two people opening the URL at the SAME exact time taking into considerating network differences on LTE/5G, WiFi etc... Also, any incorrect values can be ignored as the viewcount is purely cosmetic, I do not restrict billing/usage based on View Counts (just submission count), so if the counter is off by 1 or 2, that is not detrimental.

stone ingotBOT
#

Let me check the knowledge base for you again. One moment! :books:

#

Given your use case—where view count increments are infrequent, the value is cosmetic, and occasional inaccuracies are acceptable—using a simple mutation to increment a field is reasonable. However, Convex’s optimistic concurrency control will still throw write conflict errors if two increments happen at the same time, even if rare. This is a known limitation when updating the same document concurrently Errors and Warnings.

If you want to avoid these errors entirely, the recommended scalable solution is still the Sharded Counter component, which is designed to handle high-throughput, parallel-safe increments. But for your scenario, where conflicts are rare and accuracy is not critical, you can:

  • Continue with your current approach and simply catch and ignore OCC errors in your frontend or backend, since a missed increment is not a problem for you.
  • Optionally, implement a retry mechanism in your mutation handler to attempt the increment again if a conflict occurs, though Convex already retries a few times automatically.

Summary:

  • Your current mutation is fine for low-frequency, non-critical counters, and occasional errors can be ignored.
  • If you ever need to scale up or want to eliminate these errors, switch to the Sharded Counter component Sharded Counter.

If you want to see more about how Convex handles these conflicts and remediation strategies, see Errors and Warnings.

Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:

#

@tame heron Kapa.ai is still learning and improving, please let me know how I did by reacting below.