A Cache for Yes and No
KC Star V9 checks permissions on every calculation, and checking is expensive. So it caches the yes/no answers in a PermissionCache table with a one-hour TTL and an upsert on every miss. Here is how it works, the claimed 95 percent hit rate, and the real cost of caching authorization.
The group model collapses a thousand employees into seven groups, but it still has to answer the permission question constantly — every calculation, for every group, asks “may this group see this?” Those checks join permission tables, evaluate hierarchy rules, apply filter conditions. Doing that repeatedly for answers that rarely change is waste. So V9 caches the yes/no answers. This post is PermissionCache: how it works, the hit rate V9 claims, and the genuinely hard question of what it means to cache an authorization decision.
The cache table
The cache is a table keyed on a permission question, holding its answer and an expiry:
CREATE TABLE PermissionCache (
CacheId SERIAL PRIMARY KEY,
PermissionKey VARCHAR(255) UNIQUE, -- the question, encoded
HasPermission BOOLEAN, -- the answer
ExpiresAt TIMESTAMP, -- one hour out
AccessCount INT DEFAULT 0 -- how often this answer was reused
);
PermissionKey encodes the question — some combination of group, permission type and context — and HasPermission is the cached yes or no. ExpiresAt is set an hour ahead, so answers are trusted for up to an hour before being recomputed. AccessCount tracks reuse, which is how the system measures whether the cache is earning its keep.
Check, then cache
The access pattern is the standard cache-aside, wrapped in CheckPermissionOptimized:
CREATE OR REPLACE FUNCTION CheckPermissionOptimized(...)
RETURNS BOOLEAN AS $$
BEGIN
-- 1. look for a live cached answer
SELECT HasPermission INTO v_result
FROM PermissionCache
WHERE PermissionKey = v_key AND ExpiresAt > now();
IF FOUND THEN
UPDATE PermissionCache SET AccessCount = AccessCount + 1
WHERE PermissionKey = v_key;
RETURN v_result; -- cache hit
END IF;
-- 2. miss: compute the real answer
v_result := <expensive permission evaluation>;
-- 3. store it with a fresh TTL, upserting on the unique key
INSERT INTO PermissionCache (PermissionKey, HasPermission, ExpiresAt)
VALUES (v_key, v_result, now() + interval '1 hour')
ON CONFLICT (PermissionKey)
DO UPDATE SET HasPermission = EXCLUDED.HasPermission,
ExpiresAt = EXCLUDED.ExpiresAt;
RETURN v_result;
END;
$$ LANGUAGE plpgsql;
On a hit — a cached answer that has not expired — it bumps AccessCount and returns immediately, skipping all the permission-table joins. On a miss, it computes the real answer and upserts it with a fresh one-hour TTL. The ON CONFLICT (PermissionKey) DO UPDATE is what makes concurrent misses safe: two callers computing the same answer at once both resolve to a single cached row rather than fighting over a duplicate. Supporting functions ClearPermissionCache and GetPermissionCacheStats let you flush and inspect it, and a PermissionCacheStatisticsV9 view surfaces the hit rate.
The claimed 95%
V9 claims a “95%+ permission cache hit rate,” and the claim is plausible for the right reason. Permission answers have enormous temporal locality: the same group asks the same permission questions over and over within a short window, and the answers almost never change minute to minute. A cache exploits exactly that locality — compute each distinct answer once an hour, serve it from the table the other thousands of times. When the underlying question space is small and stable (seven groups, a handful of permission types) and the query rate is high, a 95% hit rate is entirely believable. This is the best-case shape for a cache, and permission checks fit it well.
The hard part: caching authorization is not caching data
Here is where I want to be careful, because caching a permission decision is not the same as caching a value, and the difference is a real hazard.
When you cache a computed number and the inputs change, you serve a slightly stale number — mildly wrong, usually harmless. When you cache an authorization decision and the permission changes, you can serve access that should have been revoked. The one-hour TTL means a revoked permission can remain effective for up to an hour. If you fire someone, or pull a contractor's clearance, and their “yes” is sitting in PermissionCache with forty minutes left on its TTL, they keep their access for those forty minutes. That is a genuine security consideration, not a performance footnote.
The mitigations are there — ClearPermissionCache exists precisely so a revocation can flush the cache immediately rather than waiting out the TTL — but they require the revoking code to remember to call it. A permission change that updates the grant tables but forgets to clear the cache leaves stale “yes” answers live. The cache introduces a coupling: every write that changes what someone may see must also invalidate the cache, or the cache lies. That coupling is the price of the speed, and it is easy to get wrong.
This is the same tension as the V6 read-time filtering, inverted. V6 filtered on every read, so a revocation took effect instantly — slow but always correct. V9 caches, so reads are fast but a revocation has a staleness window unless explicitly flushed. Fast-but-stale versus slow-but-fresh is the eternal cache trade, and for authorization the “stale” side has teeth that the data-caching version does not.
Was it worth it
For V9's stated goals — high-throughput permission-aware analytics — the cache is justified. The permission checks are genuinely expensive and genuinely repetitive, which is exactly the profile that caching rewards, and the group model keeps the key space small enough that the hit rate is high. The 95% claim, unlike some of the project's numbers, rests on a sound mechanism.
But it is another layer on an already tall stack — the fusion, the groups, and now the cache, each solving a problem the previous layer created. The cache exists because checking is expensive; checking is expensive because permissions are group-structured; groups exist because per-employee did not scale; per-employee came from fusing two engines. Every layer is justified by the one below it, and the tower is tall. A one-hour TTL with a manual-flush-on-revoke requirement is a lot of operational subtlety for “make permission checks fast.”
That accumulating subtlety is the exact thing V9 Simplified pushes back on. It keeps a cache too, but caches something different and, I think, better-chosen — calculation results, not permission booleans — sidestepping the revocation-staleness hazard entirely. That contrast is one of the sharpest arguments in the whole project for simplicity over cleverness.
One piece of V9's permission semantics remains, and it is subtle enough to matter: when the answer is no, what does the measure show? Not zero. NULL means you can't see it closes the V9 series.