Skip to main content
doc0blade47/drizzle-ormdocumentation home
Sign in

Query Caching

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

Query caching in Drizzle provides an extensible caching infrastructure designed to intercept database execution, minimize redundant round-trips, and maintain synchronization between cached query results and underlying table mutations. By integrating caching abstractions directly into database sessions and prepared statements across supported SQL dialects, queries can be conditionally cached, retrieved via read-through patterns, and automatically invalidated when related tables undergo changes.

Sources: drizzle-orm/src/cache/core/cache.ts:5-42, drizzle-orm/src/singlestore-core/session.ts:68-152, drizzle-orm/src/mysql-core/session.ts:70-154, drizzle-orm/src/sqlite-core/session.ts:71-155, drizzle-orm/src/pg-core/session.ts:64-147

Core Cache Abstractions and Contract

Base Cache Class and Null Implementation

export abstract class Cache {
	static readonly [entityKind]: string = 'Cache';
 
	abstract strategy(): 'explicit' | 'all';
 
	/**
	 * Invoked if we should check cache for cached response
	 * @param sql
	 * @param tables
	 */
	abstract get(
		key: string,
		tables: string[],
		isTag: boolean,
		isAutoInvalidate?: boolean,
	): Promise<any[] | undefined>;
 
	/**
	 * Invoked if new query should be inserted to cache
	 * @param sql
	 * @param tables
	 */
	abstract put(
		hashedQuery: string,
		response: any,
		tables: string[],
		isTag: boolean,
		config?: CacheConfig,
	): Promise<void>;
 
	/**
	 * Invoked if insert, update, delete was invoked
	 * @param tables
	 */
	abstract onMutate(
		params: MutationOption,
	): Promise<void>;
}
 
export class NoopCache extends Cache {
	override strategy() {
		return 'all' as const;
	}
 
	static override readonly [entityKind]: string = 'NoopCache';
 
	override async get(_key: string): Promise<any[] | undefined> {
		return undefined;
	}
	override async put(
		_hashedQuery: string,
		_response: any,
		_tables: string[],
		_config?: any,
	): Promise<void> {
		// noop
	}
	override async onMutate(_params: MutationOption): Promise<void> {
		// noop
	}
}

Sources: drizzle-orm/src/cache/core/cache.ts:5-65

Note

NoopCache inherits from the abstract Cache base class and resolves cache strategies as 'all' while returning undefined for get operations, effectively bypassing caching when no active provider is configured.

Mutation Options and Query Hashing

export type MutationOption = { tags?: string | string[]; tables?: Table<any> | Table<any>[] | string | string[] };
 
export async function hashQuery(sql: string, params?: any[]) {
	const dataToHash = `${sql}-${JSON.stringify(params)}`;
	const encoder = new TextEncoder();
	const data = encoder.encode(dataToHash);
	const hashBuffer = await crypto.subtle.digest('SHA-256', data);
	const hashArray = [...new Uint8Array(hashBuffer)];
	const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
 
	return hashHex;
}

Sources: drizzle-orm/src/cache/core/cache.ts:67-78

Important

hashQuery serializes the SQL string and parameter array using JSON.stringify, encodes the combined payload via TextEncoder, and computes a SHA-256 digest using crypto.subtle.digest, converting the resulting buffer into a hexadecimal string representation.

Configuration Interfaces

export type CacheConfig = {
	/**
	 * expire time, in seconds (a positive integer)
	 */
	ex?: number;
	/**
	 * expire time, in milliseconds (a positive integer).
	 */
	px?: number;
	/**
	 * Unix time at which the key will expire, in seconds (a positive integer).
	 */
	exat?: number;
	/**
	 * Unix time at which the key will expire, in milliseconds (a positive integer)
	 */
	pxat?: number;
	/**
	 * Retain the time to live associated with the key.
	 */
	keepTtl?: boolean;
	/**
	 * Set an expiration (TTL or time to live) on one or more fields of a given hash key.
	 * Used for HEXPIRE command
	 */
	hexOptions?: 'NX' | 'nx' | 'XX' | 'xx' | 'GT' | 'gt' | 'LT' | 'lt';
};
 
export type WithCacheConfig = { enable: boolean; config?: CacheConfig; tag?: string; autoInvalidate?: boolean };

Sources: drizzle-orm/src/cache/core/types.ts:1-29

Configuration PropertyTypeDescription
exnumberExpire time in seconds (positive integer)
pxnumberExpire time in milliseconds (positive integer)
exatnumberUnix time at which the key will expire, in seconds
pxatnumberUnix time at which the key will expire, in milliseconds
keepTtlbooleanRetain the time to live associated with the key
hexOptions'NX' | 'nx' | 'XX' | 'xx' | 'GT' | 'gt' | 'LT' | 'lt'Expiration options used for HEXPIRE commands
enablebooleanEnables or disables query caching
configCacheConfigUnderlying expiration and TTL configuration
tagstringCustom cache tag for group invalidation
autoInvalidatebooleanAutomatically invalidate entries upon table mutation

Sources: drizzle-orm/src/cache/core/types.ts:1-29

Upstash Redis Storage Implementation

UpstashCache Storage Architecture and Key Prefixes

The UpstashCache class extends the core Cache abstraction, utilizing the `@upstash/redis client package to implement distributed query caching with Lua scripting support. It relies on fixed naming prefixes and global key structures to manage cache entries, non-auto-invalidated queries, and relational table mappings.

Constant NameValue / PrefixDescription
compositeTableSetPrefix'__CTS__'Prefix for sets denoting composite table names for unique tables
compositeTablePrefix'__CT__'Prefix for Redis hashes mapping hash keys or tags to cache values
tagsMapKey'__tagsMap__'Redis key holding the mapping of tags to composite table names
nonAutoInvalidateTablePrefix'__nonAutoInvalidate__'Hash prefix storing queries whose auto-invalidation is disabled

Sources: drizzle-orm/src/cache/upstash/cache.ts:60-95

Note

Queries configured with auto-invalidation disabled bypass composite table hashes entirely, preventing them from being swept away during table mutation triggers and instead residing inside the __nonAutoInvalidate__ Redis hash.

Sources: drizzle-orm/src/cache/upstash/cache.ts:87-94

Lua Scripting for Retrieval and Mutation

const getByTagScript = `
local tagsMapKey = KEYS[1] -- tags map key
local tag        = ARGV[1] -- tag
 
local compositeTableName = redis.call('HGET', tagsMapKey, tag)
if not compositeTableName then
  return nil
end
 
local value = redis.call('HGET', compositeTableName, tag)
return value
`;
 
const onMutateScript = `
local tagsMapKey = KEYS[1] -- tags map key
local tables     = {}      -- initialize tables array
local tags       = ARGV    -- tags array
 
for i = 2, #KEYS do
  tables[#tables + 1] = KEYS[i] -- add all keys except the first one to tables
end
 
if #tags > 0 then
  for _, tag in ipairs(tags) do
    if tag ~= nil and tag ~= '' then
      local compositeTableName = redis.call('HGET', tagsMapKey, tag)
      if compositeTableName then
        redis.call('HDEL', compositeTableName, tag)
      end
    end
  end
  redis.call('HDEL', tagsMapKey, unpack(tags))
end
 
local keysToDelete = {}
 
if #tables > 0 then
  local compositeTableNames = redis.call('SUNION', unpack(tables))
  for _, compositeTableName in ipairs(compositeTableNames) do
    keysToDelete[#keysToDelete + 1] = compositeTableName
  end
  for _, table in ipairs(tables) do
    keysToDelete[#keysToDelete + 1] = table
  end
  redis.call('DEL', unpack(keysToDelete))
end
`;

Sources: drizzle-orm/src/cache/upstash/cache.ts:8-54

Important

UpstashCache registers two compiled Lua scripts during initialization: getByTagScript (marked readonly) which looks up tag mappings and fetches cached values in a single atomic round-trip, and onMutateScript which executes set unions (SUNION), table deletions (DEL), and hash field removals (HDEL) when database modifications occur.

Sources: drizzle-orm/src/cache/upstash/cache.ts:8-54, drizzle-orm/src/cache/upstash/cache.ts:106-109

Session Cache Lifecycle Coordination

Execution Interception and Caching Mechanisms

Database sessions across SQLite, PostgreSQL, and MySQL proxy implementations coordinate caching through prepared query abstractions. When a query is executed via SQLiteRemoteSession, PgRemoteSession, or MySqlRemoteSession, parameters are filled, queries are logged via Logger, and execution flows through read-through wrapper methods like queryWithCache.

Session ClassDriver TypeCache Option PropertyDefault Cache Implementation
SQLiteRemoteSessionSQLite ProxycacheNoopCache
PgRemoteSessionPostgreSQL ProxycacheNoopCache
MySqlRemoteSessionMySQL ProxycacheNoopCache

Sources: drizzle-orm/src/sqlite-proxy/session.ts:22-41, drizzle-orm/src/pg-proxy/session.ts:18-41, drizzle-orm/src/mysql-proxy/session.ts:27-50

Note

Sessions initialize their internal logger and cache instances during construction, falling back to NoopLogger and NoopCache when options are omitted from the session configuration object.

Sources: drizzle-orm/src/sqlite-proxy/session.ts:38-41, drizzle-orm/src/pg-proxy/session.ts:32-41, drizzle-orm/src/mysql-proxy/session.ts:41-50

Prepared Query Coordination

Prepared query classes wrap execution commands (run, all, get, values, execute) and interact directly with session-level caching hooks by passing query strings, parameters, and execution callbacks.

async run(placeholderValues?: Record<string, unknown>): Promise<SqliteRemoteResult> {
	const params = fillPlaceholders(this.query.params, placeholderValues ?? {});
	this.logger.logQuery(this.query.sql, params);
	return await this.queryWithCache(this.query.sql, params, async () => {
		return await (this.client as AsyncRemoteCallback)(this.query.sql, params, 'run');
	});
}

Sources: drizzle-orm/src/sqlite-proxy/session.ts:177-183

Warning

In PgRemoteSession, query execution wrapped with OpenTelemetry spans (drizzle.execute and drizzle.driver.execute) records query text and parameter payloads as span attributes before delegating to queryWithCache.

Sources: drizzle-orm/src/pg-proxy/session.ts:113-154

Cache Invalidation and Mutation Handling

Mutation Options and Invalidation Triggers

The core cache abstract contract defines the onMutate lifecycle hook to handle database modification operations such as inserts, updates, and deletes [Cache Invalidation and Mutation Handling], accepting a MutationOption parameter [Cache Invalidation and Mutation Handling]. This parameter structure supports optional tags and tables specified as single items or arrays of strings, table objects, or table instances [Cache Invalidation and Mutation Handling].

export type MutationOption = { tags?: string | string[]; tables?: Table<any> | Table<any>[] | string | string[] };

Sources: drizzle-orm/src/cache/core/cache.ts:67-67

Note

The NoopCache implementation provides an empty implementation for onMutate, safely ignoring table mutation events when caching is disabled or unconfigured [Cache Invalidation and Mutation Handling].

Sources: drizzle-orm/src/cache/core/cache.ts:62-64

Upstash Mutation Execution via Lua

When database modifications trigger cache invalidation in UpstashCache, execution delegates to a specialized Lua script (onMutateScript) deployed to Redis [Cache Invalidation and Mutation Handling]. This script evaluates keys and arguments atomically to prune obsolete cache entries across composite table hashes and tag mapping structures [Cache Invalidation and Mutation Handling].

Sources: drizzle-orm/src/cache/upstash/cache.ts:21-54

Caution

The onMutateScript executes a set union (SUNION) across specified table keys to gather all composite table names linked to modified tables, subsequently issuing batch DEL commands to purge stale query results from Redis in a single atomic operation [Cache Invalidation and Mutation Handling].

Sources: drizzle-orm/src/cache/upstash/cache.ts:42-53

Driver and Serverless Runtime Adapters

Session Options and NoopCache Wiring

Serverless and lightweight driver sessions initialize their options by defaulting unconfigured cache properties to NoopCache. Across driver implementations including TiDBServerlessSession, NeonSession, XataHttpSession, GelDbSession, PgliteSession, OPSQLiteSession, and AwsDataApiSession, the session constructor extracts the optional cache from the session options object or instantiates a fresh NoopCache instance.

	private options: TiDBServerlessSessionOptions = {},
) {
	super(dialect);
	this.client = tx ?? baseClient;
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/tidb-serverless/session.ts:124-130

constructor(
	private client: XataHttpClient,
	dialect: PgDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	private options: XataHttpSessionOptions = {},
) {
	super(dialect);
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/xata-http/session.ts:110-119

constructor(
	private client: GelClient,
	dialect: GelDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	private options: GelSessionOptions = {},
) {
	super(dialect);
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/gel/session.ts:117-126

 
constructor(
	private client: PgliteClient | Transaction,
	dialect: PgDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	private options: PgliteSessionOptions = {},
) {
	super(dialect);
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/pglite/session.ts:131-141

constructor(
	private client: OPSQLiteConnection,
	dialect: SQLiteAsyncDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	options: OPSQLiteSessionOptions = {},
) {
	super(dialect);
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/op-sqlite/session.ts:37-46

constructor(
	/** @internal */
	readonly client: AwsDataApiClient,
	dialect: PgDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	private options: AwsDataApiSessionOptions,
	/** @internal */
	readonly transactionId: string | undefined,
) {
	super(dialect);
	this.rawQuery = {
		secretArn: options.secretArn,
		resourceArn: options.resourceArn,
		database: options.database,
	};
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/aws-data-api/pg/session.ts:175-191

constructor(
	public client: TSQL,
	dialect: PgDialect,
	private schema: RelationalSchemaConfig<TSchema> | undefined,
	/** @internal */
	readonly options: BunSQLSessionOptions = {},
) {
	super(dialect);
	this.logger = options.logger ?? new NoopLogger();
	this.cache = options.cache ?? new NoopCache();
}

Sources: drizzle-orm/src/bun-sql/session.ts:120-130

Note

Synchronous storage environments such as SQLiteDOSession omit cache constructor arguments entirely within their prepared query initializers because synchronous runtime caching is unsupported.

Sources: drizzle-orm/src/durable-sqlite/session.ts:100-111

Prepared Query Cache Propagation

When sessions instantiate prepared queries, the session-level cache reference is passed directly into the query constructor. Prepared query classes such as TiDBServerlessPreparedQuery, NeonPreparedQuery, LibSQLPreparedQuery, VercelPgPreparedQuery, XataHttpPreparedQuery, GelDbPreparedQuery, PglitePreparedQuery, OPSQLitePreparedQuery, NeonHttpPreparedQuery, AwsDataApiPreparedQuery, and BunSQLPreparedQuery forward the cache instance, query metadata, and cache configuration objects up to base query classes.

Driver Prepared QueryClient / Driver TypeSession TypeCache Support
TiDBServerlessPreparedQueryTx | ConnectionTiDBServerlessSessionYes
NeonPreparedQueryPool | PoolClient | ClientNeonSessionYes
LibSQLPreparedQueryClientLibSQLSessionYes
VercelPgPreparedQueryVercelPool | VercelClient | VercelPoolClientVercelPgSessionYes
XataHttpPreparedQueryXataHttpClientXataHttpSessionYes
GelDbPreparedQueryClient | TransactionGelDbSessionYes
PglitePreparedQueryPgliteClient | TransactionPgliteSessionYes
OPSQLitePreparedQueryOPSQLiteConnectionOPSQLiteSessionYes
NeonHttpPreparedQueryNeonHttpClientNeonHttpSessionYes
AwsDataApiPreparedQueryAwsDataApiClientAwsDataApiSessionYes
BunSQLPreparedQuerySQLBunSQLSessionYes
SQLiteDOPreparedQueryDurableObjectStorageSQLiteDOSessionNo (undefined)

Sources: drizzle-orm/src/tidb-serverless/session.ts:26-48, drizzle-orm/src/neon-serverless/session.ts:27-49, drizzle-orm/src/libsql/session.ts:165-192, drizzle-orm/src/vercel-postgres/session.ts:26-48, drizzle-orm/src/xata-http/session.ts:27-45, drizzle-orm/src/gel/session.ts:17-37, drizzle-orm/src/pglite/session.ts:19-42, drizzle-orm/src/op-sqlite/session.ts:112-133, drizzle-orm/src/neon-http/session.ts:29-48, drizzle-orm/src/aws-data-api/pg/session.ts:30-55, drizzle-orm/src/bun-sql/session.ts:19-38, drizzle-orm/src/durable-sqlite/session.ts:90-111

Warning

When transaction boundaries are opened in serverless sessions—such as TiDBServerlessSession.transaction, GelDbSession.transaction, PgliteSession.transaction, and AwsDataApiSession.transaction—new session instances are created with native transaction handles while preserving reference to the parent session's configured cache instance.

Sources: drizzle-orm/src/tidb-serverless/session.ts:173-191, drizzle-orm/src/gel/session.ts:154-162, drizzle-orm/src/pglite/session.ts:170-187, drizzle-orm/src/aws-data-api/pg/session.ts:238-256

Was this page helpful?