File-based profile storage
Persist profiles to disk so they survive application restarts and can be inspected across debug sessions.
This tutorial shows how to configure @eleven-labs/nest-profiler to store profiles in files instead of memory, making them available across application restarts.
Why use file storage
The default in-memory storage loses all profiles when the application restarts. File-based storage persists each profile as a JSON file on disk, which is useful when:
- You want to compare profiles from different runs
- You are debugging an issue that requires restarting the server
- You need to share profile files with a teammate
- Your application under load produces more profiles than memory storage can hold
Step 1 - Configure file storage
Pass storageType: 'file' and a storagePath to ProfilerModule.forRoot():
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
@Module({
imports: [
ProfilerModule.forRoot({
isGlobal: true,
storageType: 'file',
storagePath: '.profiler',
}),
],
})
export class AppModule {}storagePath is resolved relative to process.cwd(). The directory is created automatically if it does not exist.
If storagePath is omitted, the default path .profiler (relative to the current working directory) is used.
Step 2 - Add .profiler/ to .gitignore
Profile files contain request data including headers and response bodies. Add the storage directory to .gitignore:
# Profiler storage
.profiler/
# SQLite database (when using the SQLite adapter)
*.db
*.db-wal
*.db-shmWhat gets stored
Each captured request generates one JSON file in the storage directory. The filename is the debug token (a UUID), for example:
.profiler/
550e8400-e29b-41d4-a716-446655440000.json
7d793037-a076-4021-bbde-f166db8ec533.jsonEach file contains the full profile object: request/response metadata, timing, logs, exceptions, and all collector data.
LRU eviction and TTL cleanup
File storage applies the same eviction policies as memory storage:
- LRU eviction - when the maximum number of stored profiles is reached, the oldest (least recently accessed) file is deleted to make room for the new one.
- TTL cleanup - on module initialization, files older than the configured TTL are deleted. Stale files are not served even if they still exist on disk.
Both limits are configured via ProfilerModule.forRoot():
ProfilerModule.forRoot({
storageType: 'file',
storagePath: '.profiler',
maxProfiles: 100, // default: 100
ttl: 3600, // default: 3600 (seconds — 1 hour)
});maxProfiles also bounds the in-memory summary index and parsed-profile cache the file storage keeps, so memory usage grows with maxProfiles × average profile size - keep it reasonable when collectBody is enabled.
SQLite storage
When you keep many profiles, filtering and paginating them in memory adds up. The package ships a SQLite adapter under the @eleven-labs/nest-profiler/sqlite subpath (backed by @libsql/client) that pushes filtering, sorting and pagination down to the database (WHERE … ORDER BY … LIMIT/OFFSET with a COUNT(*) total), so a list render never loads the whole store. Like file storage it survives restarts and is cross-process (WAL), so CLI command profiles show up in the web profiler. The same adapter also points at a remote SQLite database (url + optional authToken) — handy on serverless hosts.
It is opt-in: @libsql/client is an optional peer dependency, so memory/file users pull nothing extra.
pnpm add @libsql/clientPass it through the storage option (not storageType) so the core module never imports the driver:
import { Module } from '@nestjs/common';
import { ProfilerModule } from '@eleven-labs/nest-profiler';
import { SqliteStorageAdapter } from '@eleven-labs/nest-profiler/sqlite';
@Module({
imports: [
ProfilerModule.forRoot({
isGlobal: true,
storage: new SqliteStorageAdapter({
path: '.profiler/profiler.db', // relative to cwd; ':memory:' for an ephemeral DB
maxProfiles: 500,
ttl: 3600, // seconds
}),
}),
],
})
export class AppModule {}To store profiles in a remote SQLite database instead — no separate adapter — pass url (and an optional authToken); it takes precedence over path:
storage: new SqliteStorageAdapter({
url: process.env.PROFILER_STORAGE_URL!, // libsql://… endpoint
authToken: process.env.PROFILER_STORAGE_AUTH_TOKEN, // if the server requires one
maxProfiles: 500,
ttl: 3600,
}),Each profile becomes a row with indexed summary columns (type, method, status, duration, exceptions, a search column and kind-specific attributes as JSON) plus the full profile document. A local file database is cross-process (WAL); :memory: is single-connection; a url is a remote, cross-process database. For a local file, add it to .gitignore (*.db, *.db-wal, *.db-shm) — it holds captured request data.
Custom storage adapter
If none of the built-in backends fits your needs (for example, you want to store profiles in Redis), implement IProfilerStorageAdapter — save, findAll, findOne and clear:
import type { IProfilerStorageAdapter, Profile } from '@eleven-labs/nest-profiler';
export class RedisProfilerStorageAdapter implements IProfilerStorageAdapter {
async save(profile: Profile): Promise<void> {
await this.redis.set(`profiler:${profile.token}`, JSON.stringify(profile), 'EX', 3600);
}
async findOne(token: string): Promise<Profile | undefined> {
const raw = await this.redis.get(`profiler:${token}`);
return raw ? (JSON.parse(raw) as Profile) : undefined;
}
async findAll(): Promise<Profile[]> {
const keys = await this.redis.keys('profiler:*');
const results = await Promise.all(keys.map((k) => this.redis.get(k)));
return results
.filter((r): r is string => Boolean(r))
.map((r) => JSON.parse(r) as Profile)
.sort((a, b) => b.createdAt - a.createdAt);
}
async clear(): Promise<void> {
const keys = await this.redis.keys('profiler:*');
if (keys.length > 0) await this.redis.del(...keys);
}
}Register the adapter through the storage option (it takes precedence over storageType):
ProfilerModule.forRoot({
isGlobal: true,
storage: new RedisProfilerStorageAdapter(redisClient),
});For a large backing store, additionally implement the optional query(), distinct() and setIndexAttributesProvider() methods to push filtering and pagination down to your store — see the Storage backends reference. When they are absent the profiler falls back to filtering in memory over findAll().
Production warning
Profile files contain sensitive request data. Never enable the profiler - regardless of storage
type - in production environments. Gate it with ConditionalModule (see Getting
started).