Conflict Free Counter

Conflict-free counter for Convex that appends writes to a log instead of updating a shared row, so concurrent mutations never contend or retry.

Installation

npm install convex-conflict-free-counter

About Conflict Free Counter

Conflict Free Counter provides an eventually-consistent, contention-free counter built on a conflict-free log architecture. Every call to `add` appends a row to an internal log rather than updating a shared document, eliminating write contention entirely. A scheduled compaction pipeline periodically folds the log into a snapshot, and reads return `snapshot + visible log entries` along with a `fullyConsistent` flag indicating whether the returned count reflects every committed write.

Benefits

Use cases

how to increment a counter from many concurrent Convex mutations without conflicts

The convex-conflict-free-counter component appends each increment to an internal log rather than updating a shared document, so concurrent mutations never conflict. Call counter.add(ctx, 'my-key') from any mutation or action and writes are durably committed without retries. A scheduled compaction pipeline folds the log into a snapshot periodically to keep read costs low.

eventually consistent counter in Convex for metrics and event tracking

convex-conflict-free-counter is designed for metrics, event counts, and tallies where many writers update the same key. Reads return both a count and a fullyConsistent boolean: true means the count reflects every write committed before the read, false means the log backlog exceeded the scan budget and the count may lag by up to the compaction delay (default 15 seconds). Every write is durable and the count always converges.

Convex counter that does not slow down mutations under high write load

Because convex-conflict-free-counter appends to a log rather than mutating a single row, there is no OCC (optimistic concurrency control) contention between writers. Throughput scales with write volume automatically, unlike sharded counters where you must pre-select a shard count. Use counter.addMany to batch multiple key updates into a single component call.

how to read a Convex counter without re-running the query on every write

Pass logScanLimit: 0 to counter.count() to read only the compacted snapshot, skipping the live log. This means the query only re-runs once per compaction cycle rather than on every increment, which reduces reactive subscription overhead in high-write scenarios.

Frequently asked questions

How is convex-conflict-free-counter different from the Convex sharded counter component?

convex-conflict-free-counter does not require you to choose a shard count upfront or rebalance shards as load changes. Throughput scales automatically because every write appends a new log row rather than contending on a fixed set of shards. The trade-off is that reads can lag by up to the compaction delay (default 15 seconds) during high write bursts, while sharded-counter trades read contention against shard count instead.

Are writes ever lost if the compaction has not run yet?

No. Every call to counter.add() is committed in the calling mutation's transaction before the function returns. The compaction pipeline only consolidates existing log entries into a snapshot; it does not gate durability. The count always converges to the true total regardless of compaction timing.

What does the fullyConsistent flag mean on counter.count()?

fullyConsistent: true means the returned count includes every write committed before the query started, because the snapshot plus the scanned log tail covered all entries. fullyConsistent: false means more log entries existed than the read scanned, so the count may temporarily be missing recent increments. The lag is bounded by the compactionDelay setting, which defaults to 15 seconds.

How do I install and register the convex-conflict-free-counter component?

Run npm install convex-conflict-free-counter, then add app.use(conflictFreeCounter) in your convex/convex.config.ts file after importing from convex-conflict-free-counter/convex.config. Instantiate the client with new ConflictFreeCounter(components.conflictFreeCounter) in a shared module and import that instance in your mutation and query files.

Can I use convex-conflict-free-counter to subtract from a counter or track negative deltas?

Yes. The delta argument to counter.add() accepts any Convex number, including negative values. For example, counter.add(ctx, 'credits:remaining', -3) subtracts 3 from that key. Deltas are IEEE 754 doubles, so integer counts remain exact up to 2^53.

Links