← Back to Architecture
ARCH-013 Architecture 18 min read For: Technical Architects

The Truth About Salesforce Caching: What's Cached, What Isn't

Caching in Salesforce is not a single mechanism — it is a layered set of platform features and custom patterns, each with different scope, TTL, and eviction behaviour. Misunderstanding caching is responsible for both performance problems and stale data bugs in production.

VS

Vishal Sharma

Salesforce Architecture Specialist · Updated May 2026

What you will learn...
  • The four caching layers in Salesforce and what each one caches
  • Platform Cache: org partition vs session partition, TTL configuration, and when to use it
  • Lightning Data Service: how it caches record data client-side and when caches go stale
  • What is NOT cached — the missing SOQL result cache and its performance implications
  • Static resource caching: CDN, browser cache, and version busting strategies
  • Cache invalidation patterns and the architectural pitfalls of stale cache data

The Four Caching Layers in Salesforce

When Salesforce architects discuss caching, they are almost never talking about a single mechanism. Salesforce caching operates across four distinct layers, each with different scope, lifetime, invalidation behaviour, and use cases. Conflating these layers is one of the most common sources of performance misconceptions and stale data bugs.

Layer 1 — Platform Cache: A server-side key-value cache managed by Apex or Flow. Stores serialised Apex objects, strings, or arbitrary data. Has org-scoped (shared across all sessions) and session-scoped (per user session) partitions. TTL is configurable per cache put operation. This is the layer developers control explicitly.

Layer 2 — Lightning Data Service (LDS): A client-side record cache managed by the Lightning framework. LDS caches the field values of Salesforce records accessed by Lightning components and shares them across components on the same page. Changes to records via LDS automatically propagate to all LDS consumers on the page. This is implicit caching managed by the framework.

Layer 3 — Static Resource Cache: CSS, JavaScript, and image files served as Salesforce static resources are cached by the browser and the Salesforce CDN. This is a standard HTTP caching layer — no Salesforce-specific behaviour, but with version busting implications for deployments.

Layer 4 — Salesforce Internal Infrastructure Caching: Metadata, security configuration, and system setup data is cached within the Salesforce application servers. This is invisible to developers but explains why metadata changes (new fields, permission changes) sometimes require a few minutes to propagate across all platform nodes.

💡
The important absence: Salesforce does NOT have a SOQL query result cache. Every SOQL query execution reads from the database. There is no "run this query 100 times, only the first hits the database" behaviour. If you want to avoid repeated database reads, you must cache the results explicitly using Platform Cache.

Platform Cache: The Developer-Controlled Layer

Platform Cache is the primary tool for Apex developers to reduce repeated expensive operations — SOQL queries, callouts, complex calculations — by storing their results and retrieving them on subsequent calls within the cache's TTL.

Platform Cache has two partition types with different scope and lifetime:

Org Partition: Data stored in the org partition is accessible to any Apex code running in the org, across all user sessions. This is appropriate for data that is the same for all users: reference data (static lookup values, configuration tables), external API responses that don't vary per user, and computationally expensive aggregations that are shared across users. TTL can be set from 1 second to 48 hours.

Session Partition: Data stored in the session partition is accessible only within the current user's session. This is appropriate for user-specific data: personalisation preferences, multi-step form state, permission-denormalised data specific to the current user's access context. Session cache is automatically invalidated when the session ends (logout or expiry).

// Platform Cache — storing and retrieving from org partition
public class ConfigCacheService {

    private static final String CACHE_KEY = 'configData';
    private static final Integer TTL_SECONDS = 3600; // 1 hour

    public static Map<String, String> getConfig() {
        Cache.OrgPartition orgPart = Cache.Org.getPartition('local.AppConfig');

        // Try cache first
        Map<String, String> config =
            (Map<String, String>) orgPart.get(CACHE_KEY);

        if (config == null) {
            // Cache miss — query from database
            config = loadConfigFromDatabase();
            orgPart.put(CACHE_KEY, config, TTL_SECONDS);
        }

        return config;
    }

    // Call this when config changes to force cache refresh
    public static void invalidateCache() {
        Cache.OrgPartition orgPart = Cache.Org.getPartition('local.AppConfig');
        orgPart.remove(CACHE_KEY);
    }
}
🔑
Platform Cache capacity is licensed: Platform Cache capacity (the maximum total data size across all partitions) is a licensed feature. Default orgs have limited Platform Cache capacity; additional capacity can be purchased. Partition sizes are configured in Setup under Platform Cache. Attempting to store data when the partition is full throws a CacheException — handle this defensively in production code.

Lightning Data Service: The Client-Side Record Cache

Lightning Data Service (LDS) is the framework layer that manages record data in Lightning components. When a Lightning Web Component uses @wire(getRecord) or @wire(updateRecord), LDS intercepts the call and manages a client-side cache of the record's field values.

The key architectural behaviour of LDS is shared cache across components on the same page. If three components on the same record page all use @wire(getRecord) for the same record, LDS makes one server call and shares the result across all three components. If any component updates the record via LDS, the cache is automatically invalidated and all components on the page receive the updated data — without explicit coordination code.

LDS also handles optimistic UI updates — when you call updateRecord via LDS, the UI can update immediately with the new values before the server confirms the save. If the server save fails, LDS rolls back the optimistic update. This makes Lightning interfaces feel responsive without custom "loading" state management.

// Lightning Web Component using LDS wire adaptor
import { LightningElement, api, wire } from 'lwc';
import { getRecord, getFieldValue } from 'lightning/uiRecordApi';
import ACCOUNT_NAME_FIELD from '@salesforce/schema/Account.Name';
import ANNUAL_REVENUE_FIELD from '@salesforce/schema/Account.AnnualRevenue';

export default class AccountSummary extends LightningElement {
    @api recordId;

    @wire(getRecord, {
        recordId: '$recordId',
        fields: [ACCOUNT_NAME_FIELD, ANNUAL_REVENUE_FIELD]
    })
    account;
    // LDS caches this record — other components on the page
    // sharing this recordId receive the same cached data

    get name() {
        return getFieldValue(this.account.data, ACCOUNT_NAME_FIELD);
    }
}

What Is NOT Cached: The SOQL Non-Cache Reality

Understanding what Salesforce does NOT cache is as important as understanding what it does cache — because many performance optimisation attempts are built on a false assumption about platform-level caching that doesn't exist.

SOQL query results: The most important non-cache. Salesforce has no query result cache. Executing the same SOQL query twice in the same transaction, in two different Apex methods, or in two different transactions all result in separate database reads. The second call to [SELECT Id FROM Account WHERE ...] queries the database fresh — there is no "already ran this" cache.

Aggregate results: COUNT(), SUM(), AVG() and other aggregate SOQL queries are similarly uncached. Large COUNT() queries on multi-million-record objects run against the full database on every call. This is why replacing COUNT() queries in frequently-called Apex with Platform Cache values is one of the most impactful performance optimisations available.

External callout responses: Callout results are never cached by Salesforce. Every HTTP callout to an external endpoint is a new network request. If you call a reference data API 50 times in a transaction, you make 50 callout attempts — subject to callout limits and external API rate limits. Cache callout responses in Platform Cache when the data is stable within the cache TTL window.

⚠️
The most common caching mistake: Calling a SOQL query in a loop or in a method that is called from a loop, expecting the platform to "cache" the result after the first call. It doesn't. Each iteration executes a new database query. The only way to avoid repeated SOQL is to extract the query before the loop, store results in a Map, and do Map lookups inside the loop.

Static Resource Caching and Version Management

Salesforce static resources (CSS files, JavaScript libraries, images) are served with standard HTTP caching headers. Browsers cache these files locally and the Salesforce CDN caches them at edge nodes. This is a significant performance benefit — a user who has visited a Salesforce org recently does not re-download static resources on every page load.

The problem this creates for deployments: when you update a static resource (update a JavaScript library, change a CSS file), browsers and CDN nodes that have the old version cached may continue serving the stale version until the cache TTL expires. For users who don't clear their browser cache, they may see old styling or broken JavaScript behaviour after a deployment.

Salesforce addresses this via versioned static resource URLs. Each static resource version has a unique URL that includes a version hash. Deploying a new version of a static resource creates a new URL — browser and CDN caches treat it as a new resource and request it fresh. Old cached versions of the old URL remain cached but are no longer referenced by updated pages.

The implication for LWC components: static resource version references in component HTML are automatically updated when the resource is redeployed, so the version busting is automatic. For manually referenced static resources in Visualforce or custom HTML, the version URL must be regenerated after each resource update — using $Resource.myStaticResource in Visualforce ensures the versioned URL is always used.

Cache Invalidation Patterns and Stale Data Pitfalls

Cache invalidation — knowing when cached data is no longer valid and must be refreshed — is the hardest part of working with Platform Cache. The failure mode is stale data: users see outdated values because the cache has not been invalidated after the underlying data changed.

The two primary cache invalidation patterns in Salesforce are TTL-based expiry (the cache expires automatically after the configured TTL) and explicit invalidation (the cache is explicitly removed when the source data changes).

TTL-based expiry is simple to implement but creates a maximum staleness window: users may see data that is up to [TTL] seconds old. This is acceptable for reference data that changes infrequently (postal code lists, country codes, product categories) but unacceptable for business data that must be current (account credit limits, inventory levels, approval status).

Explicit invalidation is more precise but requires that the invalidation event be reliably triggered whenever the source data changes. The standard pattern: an Apex trigger on the source object calls Cache.invalidate() when relevant fields change. This ensures the cache is cleared immediately when data changes, forcing the next reader to fetch fresh data.

// Trigger-based cache invalidation
trigger AccountCacheInvalidator on Account (after update) {
    Set<Id> changedIds = new Set<Id>();

    for (Account acc : Trigger.new) {
        Account old = Trigger.oldMap.get(acc.Id);
        // Invalidate if credit limit or status changed
        if (acc.Credit_Limit__c != old.Credit_Limit__c ||
            acc.Account_Status__c != old.Account_Status__c) {
            changedIds.add(acc.Id);
        }
    }

    if (!changedIds.isEmpty()) {
        Cache.OrgPartition orgPart =
            Cache.Org.getPartition('local.CreditData');
        for (Id accountId : changedIds) {
            orgPart.remove('credit_' + accountId);
        }
    }
}

Key Takeaways

  • Salesforce caching operates across four layers: Platform Cache (developer-controlled server-side), Lightning Data Service (client-side record cache), static resource cache (browser/CDN), and internal infrastructure cache (metadata, invisible to developers).
  • Platform Cache has org-scoped partitions (shared across all users, for reference data) and session-scoped partitions (per user, for personalised state). Capacity is licensed — handle CacheException defensively in production code.
  • Lightning Data Service caches record fields client-side and shares them across components on the same page, eliminating duplicate server calls and automatically propagating record updates to all consuming components.
  • SOQL queries are NOT cached — every execution hits the database. This is the most important caching misconception. Cache SOQL results explicitly in Platform Cache for frequently-called reference queries to avoid repeated database reads.
  • Static resource version URLs provide automatic cache busting on deployment. Use $Resource references (not hardcoded paths) to ensure components always reference the current version after deployment.
  • Cache invalidation is the hardest problem: TTL-based expiry accepts staleness up to the TTL window; trigger-based explicit invalidation provides immediate freshness at the cost of invalidation event management complexity.

Test Your Understanding

1. An Apex method runs a COUNT() query against a 50-million-record object. The method is called on every page load by 5,000 concurrent users. What is the most impactful performance optimisation?

Create a custom index on the object — COUNT() queries benefit from indexed scans on large tables
Cache the COUNT() result in Platform Cache (org partition) with an appropriate TTL — repeated COUNT() queries on 50M records are expensive database operations; caching eliminates 4,999 of 5,000 concurrent database calls
Move the COUNT() query to an async Queueable job — async queries have higher limits and execute faster for large aggregations

2. Three LWC components on the same Salesforce record page all use @wire(getRecord) for the same record. How many server-side data requests are made when the page loads?

Three — each component's wire adaptor makes an independent server call regardless of shared record ID
One — Lightning Data Service detects that all three components are requesting the same record and makes a single server call, sharing the cached result across all three consumers
Zero — record data is pre-loaded from the URL context and wire adaptors consume it without additional server calls

3. You cached an Account's credit limit in Platform Cache (org partition) with a 4-hour TTL. An admin updates the credit limit in Salesforce 30 minutes later. A transaction checks the cached value 45 minutes after the original cache. What value does it see?

The updated credit limit — Salesforce automatically invalidates Platform Cache when the underlying record changes
The original (stale) credit limit — Platform Cache with TTL-only invalidation does not auto-invalidate when source data changes. The cache will hold the old value until the 4-hour TTL expires or explicit invalidation is triggered.
An error — Platform Cache detects the underlying record has changed and throws a CacheStaleException

Discussion & Feedback