Concurrent requests can lose increments if each reads a count and writes back its own replacement. Counter operations perform the arithmetic and mutation atomically instead. Use them for replaceable counts such as article views or requests within a window—not as the only record of billing or inventory.
These examples assume an existing *cache.Cache named store and operation
context ctx. Use cache from arkoren.dev/core/service/cache and
standard-library time. A shared driver is required for replicas
to share counts; separate memory drivers each keep their own total. Focused
snippets omit routine propagation while keeping error results visible.
Updating Counts
An article-view counter needs a cache.Key[int64]. Increment it when a view
should count:
views := cache.NewKey[int64]("article-views.v1:article-73")
total, err := store.Increment(ctx, views, 1)
On success, total is the new value. A missing or expired counter starts from
zero, so its first increment by one returns one. Decrement subtracts its delta
with the same atomic behavior:
total, err = store.Decrement(ctx, views, 1)
This is useful for correcting a counted view; it is not a condition that prevents
negative totals. Counters store base-10 integer text, so store.Get(ctx, views)
can decode the count as int64. An existing noninteger value causes an error
and remains unchanged.
Increment and Decrement ignore the key’s declared write TTL. A newly created
counter has no expiry, and an existing counter keeps its current expiry. Giving
the key a five-minute policy therefore does not make these updates expire.
Expiring Windows
Request counts usually need to reset. A separate increment followed by an
expiry-setting call leaves a crash window in which the count never expires.
IncrementWithTTL updates the count and ensures an expiry in one atomic step.
For requests from an already authorized account, use a stable internal account
ID as the key component. Here accountID is that application’s string ID:
requests := cache.NewKey[int64]("export-requests.v1:" + accountID)
count, remaining, err := store.IncrementWithTTL(ctx, requests, 1, time.Minute)
On success, count is the updated total and remaining is time until expiry.
A missing entry starts from zero with a one-minute expiry. An existing entry
without expiry receives one minute; an existing expiring entry keeps its current
deadline. Later requests do not slide that deadline forward. The window begins
when expiry is first established, not at an aligned wall-clock minute.
The explicit TTL argument controls this operation, not the key’s write policy.
It must be positive; zero or a negative duration returns cache.ErrInvalidTTL.
A custom driver unable to provide atomic increment-and-expiry must return
cache.ErrUnsupported, not emulate the operation with separate calls.
Redis rounds positive counter TTLs up to whole milliseconds, including
submillisecond durations. Its result remains exact throughout int64, and
remaining lifetimes beyond time.Duration saturate at that type’s maximum.
The rate limiting middleware uses this
operation and handles request rejection for you.
Handling Failures
Check err before using the returned count or lifetime. Arithmetic overflow and
noninteger contents are rejected without changing the stored value or expiry.
That rejection is different from a transport failure: Redis might have applied
an increment before its reply was lost.
Do not blindly repeat an increment after an uncertain result; doing so can count the same action twice. Native clients can also have transport retry policies, so a single facade call or telemetry span is not proof of one backend execution. Choose native retry settings in Cache Drivers and an application recovery policy appropriate to the count. Exactly-once business effects require a durable application protocol, not a cache counter.
No expiry does not guarantee retention. Backend loss, eviction, deletion, or restart can erase counts and reset allowances. A cache-based limit must account for those failures rather than treating a successful update as permanent state.
Integer Bounds
All three counter operations accept only mathematical results from
math.MinInt64 through math.MaxInt64, inclusive. An out-of-range result returns
a backend-native error: there is no portable overflow sentinel. Rejection leaves
the target value and TTL unchanged; a missing or expired target remains absent.
In particular, overflow in IncrementWithTTL does not add expiry to a persistent
counter.
Subtraction checks the mathematical result directly, without first negating the
delta. With standard-library math, initialize an adjustment counter at its
minimum:
adjustment := cache.NewKey[int64]("view-adjustments.v1:article-73")
err := store.Put(ctx, adjustment, math.MinInt64)
After that successful write, subtracting the same minimum is representable:
total, err := store.Decrement(ctx, adjustment, math.MinInt64)
On success, total is zero. Subtracting math.MinInt64 from zero instead fails
and leaves zero unchanged; doing so against an absent target fails without
creating it. Do not replace subtraction with Increment(ctx, key, -delta):
negating math.MinInt64 overflows in application code before the driver sees it.
Nor should callers replace checked atomic arithmetic with a read, local range
check, and write; concurrent updates can invalidate that check.
For every counter operation and error, see the cache API reference.