Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

pacecache is a generic, bounded, in-process cache for Go. Its author did not build it to be better than every existing Go cache library. The author built it to make a specific set of trade-offs explicit: lock contention against capacity balance, entry counts against memory use, logical expiry against physical cleanup, and coalesced loading against stale publication. Those trade-offs decide what the cache does well and where it stops, so they are the right place to start for anyone deciding whether to use it.

What pacecache is, and what it is not

Every process that imports pacecache owns its own cache state. Nothing is shared between service instances, nothing is persisted to disk, and there is no central invalidation channel. If two replicas of a service each cache the same row, each replica holds its own copy and each one can drift from the other until its entry expires or is replaced.

The author frames this as a deliberate boundary. When several instances need one coordinated cache, the article points to Redis or another distributed system as solving a different problem, and states plainly that pacecache is not a drop-in distributed cache. An in-process cache suits data that is safe to hold locally, where avoiding a network hop matters, where an upstream lookup is costly enough to benefit from cache-aside loading, where per-instance cache contents are acceptable, and where the application wants a bounded local hot set.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Capacity is an entry budget, not a memory ceiling

The default capacity is up to 10,000 entries, held in one segment, with no time-based expiration. The limit counts entries, not bytes. A cache holding 10,000 small integers and a cache holding 10,000 multi-kilobyte documents both count as 10,000 entries, so the process’s actual heap use depends on the size of the stored values plus per-entry overhead.

If you need a hard memory bound, size the cache by entry count against the average value size you measure in your own workload, and verify the result with a heap profile. Do not treat the entry limit as a byte limit.

The article’s illustrative larger configuration uses 100,000 entries across 64 segments. That is an example of how the settings fit together, not a recommended production value; the total capacity is divided among the segments.

Segmentation: contention against local capacity

Each segment owns its own storage, LRU list, expiration index, and lock. Splitting keys across segments means unrelated keys are less likely to wait on the same lock. The cost is that total capacity is apportioned across segments, so every segment has a local limit.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The trade-off is easiest to see side by side:

Setting Lock contention Capacity behavior Main risk
One segment (default) All keys share one lock A single LRU list uses the full entry budget Contention can rise under heavy parallel access
Many segments Unrelated keys are more likely to use different locks Each segment evicts against its own share of the budget A skewed key distribution can evict in one segment while another has free capacity

The author’s reason for defaulting to one segment is that a segment count chosen without knowing the workload can be wrong in either direction. In the author’s words, “The right segment count depends on the workload. It’s something worth measuring rather than guessing.” Measure contention and hit ratio at a few segment counts before settling on one.

Two further points follow from this design. The LRU order is exact only within a segment, not across the whole cache. And the segment count is a tuning decision that should be revisited when the key distribution changes, not a one-time setting.

Expiration is a validity rule, and cleanup is separate

The article’s central distinction is that an entry can be expired while its storage still exists. In the author’s words, “An entry being expired is not the same thing as that entry already being physically removed from storage.” TTL validity is enforced when the entry is looked up, not when a background process happens to run.

Physical removal can happen in three ways:

  • On lookup. A read that encounters an expired entry treats it as a miss and removes it.
  • Explicitly. The application calls a cleanup operation on demand.
  • Optionally, in the background. A background cleanup reclaims expired entries that are never read again.

The author prefers this separation: “scheduling cleanup and enforcing expiration are two different concerns.” A cache that never starts background cleanup still returns correct results. Its memory for never-read expired entries is reclaimed only through the other two paths.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Two expiry options refine the behavior:

  • Jitter adds a random duration below a configured limit when an expiring entry is stored. It spreads out deadlines that would otherwise line up and expire together. The illustrative configuration in the article uses a 5-minute TTL with 30 seconds of jitter.
  • Sliding expiration refreshes an entry on each successful read, using the effective TTL already chosen for that entry rather than a new value.

Entries can also be stored with no expiration at all, which is the default for a cache built without a TTL. The project README documents the TTL, jitter, sliding expiration, refresh, and no-expiration options, and describes the same lazy-expiry and optional-cleanup model.

Cache-aside loading: coalescing is not publication ordering

GetOrLoadFunc takes a loader for each call. The behavior for a miss works as follows:

  1. The first caller for a missing key runs the loader.
  2. Concurrent callers for the same key wait for that one loader execution and share its result, so the upstream is queried once.
  3. Callers for different keys load independently and do not wait on each other.
  4. A successful result that was found is cached. A not-found result and a loader error are not cached, so the next call tries the loader again.
  5. Each waiting caller keeps its own context. If one caller stops waiting, the load continues for the others.

Coalescing prevents duplicate work. It does not, by itself, stop an older load from overwriting newer data. The article describes publication barriers around the mutating operations Set, GetOrSet, Delete, and Clear. Consider this sequence: a loader starts reading the database for key k, then another goroutine calls Set on k, then the loader finishes successfully.

  • The newer mutation wins. The stale loaded result is discarded, and the load call returns ErrLoadSuperseded.
  • If the loader itself fails, that loader error takes precedence over the supersession outcome.

The project README describes the same rule: a newer mutation takes precedence over a stale loaded result. Callers that receive ErrLoadSuperseded should treat it as “a newer value already exists” and read the cache again rather than retrying the loader blindly.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Stats and OpenTelemetry

Stats() returns a detached snapshot of cache state and activity. Because each segment has its own lock, reads across segments are not one globally atomic instant. Treat the numbers as consistent per segment and approximately consistent overall, which is sufficient for dashboards and trend analysis but not for exact accounting across the cache.

Optional OpenTelemetry integration is available through extra/paceotel. The application still owns the OpenTelemetry SDK lifecycle and exporter configuration.

What the benchmark materials establish

The article frames benchmarking around three separate questions, and each needs its own test:

  • Concurrent throughput, measured with 8 workers in the project’s benchmark configuration.
  • Hit ratio under a skewed access pattern, measured over 1,000,000 requests.
  • Live heap after populating the cache with fixed-size keys and values of 32 bytes each.

The README reports the benchmark hardware as an Intel Core i7-12700H with 14 cores and 20 threads. The reviewed material describes the test dimensions and methodology, but it does not publish result figures. No independent benchmark or production adoption study was found, so no claim of performance superiority over other Go cache libraries is supported. To choose between libraries, run throughput, hit ratio, and heap measurements against your own key distribution, value sizes, and access pattern.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Getting started and licensing

pacecache is a free, open-source Go module distributed under the MIT license. The README covers installation with the Go module tooling, plus examples and documentation. No hardware or paid service is needed to use it.

The cache is most useful when you can answer three questions before adopting it: whether each instance may hold its own copy of the data, whether an entry-count budget is an acceptable proxy for your memory limit, and whether the workload’s key distribution is uneven enough to make per-segment capacity a concern.

Summary of the design decisions

pacecache’s design can be summarized as a set of explicit choices. Capacity counts entries, so memory use must be measured separately. Segmentation is a contention tool that trades lock sharing for local capacity balance. Expiration is decided on lookup, with cleanup as an optional reclamation mechanism. Coalescing removes duplicate loads, and a separate publication rule keeps a stale load from overwriting a newer mutation. Each of these choices favors some workloads over others, which is the author’s central point.

Attribution note: the design account is the pacecache maintainer’s first-person write-up, syndicated on Dev.to in 2026, and the implementation details are the author’s description, cross-checked against the project README. The quotations above are the author’s own words.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.