All Docs
FeaturesDepositClearUpdated March 12, 2026

Performance Deep-Dive: Why We're Caching Stable Data

Performance Deep-Dive: Why We're Caching Stable Data

Release: v0.1.179 · Control: PERF-18 · Category: perf_data_patterns

The Problem

Not every piece of data in the platform changes frequently. Billing plans are updated a handful of times a year. Compliance rule definitions are tied to specific legislation. Help articles are edited occasionally. Org settings and subscription status might change once in a billing cycle.

Despite this, our audit found that the platform currently has no server-side caching layer for any of these data types. Every tRPC call — regardless of how static the underlying data is — fires a fresh database query or re-executes rule logic from scratch.

This is a correctness-safe approach, but it leaves significant performance on the table, especially on high-frequency code paths.

What the Audit Found

Hot path: orgProcedure

Every authenticated, organisation-scoped API call passes through orgProcedure. As part of that middleware, the platform fetches the current subscription status for the organisation. Because orgProcedure wraps nearly all tRPC routes, this single database query is one of the most frequently executed queries in the entire system — and it runs fresh every single time.

Redundant reads for org-agnostic data

Billing plans are shared across all organisations and almost never change. Today they are re-read from the database on every request that needs them. There is no shared result, no in-flight deduplication, and no TTL-based cache.

Already-correct patterns in lib/compliance/

It's worth noting what is not a problem: compliance rule definitions and help articles are already implemented as static in-memory arrays inside lib/compliance/. These are loaded once at server startup and never re-fetched. This is the right pattern — the audit confirms these should stay as-is.

The Fix

Two complementary caching tools cover the different scenarios:

1. unstable_cache — for org-agnostic, database-sourced data

Next.js's unstable_cache wraps an async function with a persistent, TTL-based server-side cache. It's the right tool for data that is shared across all users and changes infrequently.

import { unstable_cache } from 'next/cache';
import { db } from '@/lib/db';
import { plans } from '@/lib/db/schema';

export const getPlans = unstable_cache(
  async () => db.select().from(plans),
  ['plans'],
  { revalidate: 3600 } // re-fetch at most once per hour
);

With this in place, billing plan data is fetched once, cached at the server level, and automatically invalidated after one hour. All requests in that window share the same result with zero additional database load.

2. React.cache() — for per-org data within a single request

React's built-in cache() function deduplicates calls within a single server render or request lifecycle. It doesn't persist across requests, making it safe for per-organisation data like settings and subscription status — data that is stable within a request but may differ between organisations.

import { cache } from 'react';
import { db } from '@/lib/db';
import { subscriptions } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

export const getSubscriptionStatus = cache(async (orgId: string) => {
  return db
    .select()
    .from(subscriptions)
    .where(eq(subscriptions.orgId, orgId));
});

If orgProcedure calls getSubscriptionStatus and a downstream resolver calls it again for the same orgId in the same request, the second call returns the memoised result instantly — no second database round-trip.

For subscription status specifically, combining React.cache() with a 60-second unstable_cache TTL would reduce database load further by sharing results across concurrent requests for the same organisation.

Recommended Cache Strategy by Data Type

DataChanges how often?Recommended approachTTL
Billing plansRarely (manual update)unstable_cache1 hour
Compliance rule definitionsOn legislation changeAlready static in-memory ✓
Help articlesOccasionallyAlready static in-memory ✓
Org settingsRarelyReact.cache()Per request
Subscription statusMonthly (billing cycle)React.cache() + unstable_cache60 seconds

Why This Matters

The platform is designed to handle deposit checks, compliance assessments, and dispute resolution — workflows where multiple tRPC calls fan out in parallel. Each of those calls currently pays the full cost of fetching subscription status and plan data independently. As usage grows, that overhead compounds.

Introducing even a 60-second cache on orgProcedure's subscription check would turn what is currently the platform's single most-repeated database query into an occasional background refresh.

What Stays the Same

  • No user-visible behaviour changes. Caching is transparent.
  • Compliance rule logic is unaffected — it's already static.
  • Cache invalidation on plan or subscription updates will need to pair with revalidateTag() to ensure consistency when data does change.