---
description: Caching rules for performance improvements
globs: 
alwaysApply: false
---
# Cache Optimization Patterns for Formbricks

## Cache Strategy Overview

Formbricks uses a **hybrid caching approach** optimized for enterprise scale:

- **Redis** for persistent cross-request caching  
- **React `cache()`** for request-level deduplication
- **NO Next.js `unstable_cache()`** - avoid for reliability

## Key Files

### Core Cache Infrastructure
- [apps/web/modules/cache/lib/service.ts](mdc:apps/web/modules/cache/lib/service.ts) - Redis cache service
- [apps/web/modules/cache/lib/withCache.ts](mdc:apps/web/modules/cache/lib/withCache.ts) - Cache wrapper utilities
- [apps/web/modules/cache/lib/cacheKeys.ts](mdc:apps/web/modules/cache/lib/cacheKeys.ts) - Enterprise cache key patterns and utilities

### Environment State Caching (Critical Endpoint)
- [apps/web/app/api/v1/client/[environmentId]/environment/route.ts](mdc:apps/web/app/api/v1/client/[environmentId]/environment/route.ts) - Main endpoint serving hundreds of thousands of SDK clients
- [apps/web/app/api/v1/client/[environmentId]/environment/lib/data.ts](mdc:apps/web/app/api/v1/client/[environmentId]/environment/lib/data.ts) - Optimized data layer with caching

## Enterprise-Grade Cache Key Patterns

**Always use** the `createCacheKey` utilities from [cacheKeys.ts](mdc:apps/web/modules/cache/lib/cacheKeys.ts):

```typescript
// ✅ Correct patterns
createCacheKey.environment.state(environmentId)     // "fb:env:abc123:state"
createCacheKey.organization.billing(organizationId) // "fb:org:xyz789:billing"
createCacheKey.license.status(organizationId)       // "fb:license:org123:status"
createCacheKey.user.permissions(userId, orgId)      // "fb:user:456:org:123:permissions"

// ❌ Never use flat keys - collision-prone
"environment_abc123"
"user_data_456"
```

## When to Use Each Cache Type

### Use React `cache()` for Request Deduplication
```typescript
// ✅ Prevents multiple calls within same request
export const getEnterpriseLicense = reactCache(async () => {
  // Complex license validation logic
});
```

### Use `withCache()` for Simple Database Queries
```typescript
// ✅ Simple caching with automatic fallback (TTL in milliseconds)
export const getActionClasses = (environmentId: string) => {
  return withCache(() => fetchActionClassesFromDB(environmentId), {
    key: createCacheKey.environment.actionClasses(environmentId),
    ttl: 60 * 30 * 1000, // 30 minutes in milliseconds
  })();
};
```

### Use Explicit Redis Cache for Complex Business Logic
```typescript
// ✅ Full control for high-stakes endpoints
export const getEnvironmentState = async (environmentId: string) => {
  const cached = await environmentStateCache.getEnvironmentState(environmentId);
  if (cached) return cached;
  
  const fresh = await buildComplexState(environmentId);
  await environmentStateCache.setEnvironmentState(environmentId, fresh);
  return fresh;
};
```

## Caching Decision Framework

### When TO Add Caching

```typescript
// ✅ Expensive operations that benefit from caching
- Database queries (>10ms typical)
- External API calls (>50ms typical)  
- Complex computations (>5ms)
- File system operations
- Heavy data transformations

// Example: Database query with complex joins (TTL in milliseconds)
export const getEnvironmentWithDetails = withCache(
  async (environmentId: string) => {
    return prisma.environment.findUnique({
      where: { id: environmentId },
      include: { /* complex joins */ }
    });
  },
  { key: createCacheKey.environment.details(environmentId), ttl: 60 * 30 * 1000 } // 30 minutes
)();
```

### When NOT to Add Caching

```typescript
// ❌ Don't cache these operations - minimal overhead
- Simple property access (<0.1ms)
- Basic transformations (<1ms)
- Functions that just call already-cached functions
- Pure computation without I/O

// ❌ Bad example: Redundant caching
const getCachedLicenseFeatures = withCache(
  async () => {
    const license = await getEnterpriseLicense(); // Already cached!
    return license.active ? license.features : null; // Just property access
  },
  { key: "license-features", ttl: 1800 * 1000 } // 30 minutes in milliseconds
);

// ✅ Good example: Simple and efficient
const getLicenseFeatures = async () => {
  const license = await getEnterpriseLicense(); // Already cached
  return license.active ? license.features : null; // 0.1ms overhead
};
```

### Computational Overhead Analysis

Before adding caching, analyze the overhead:

```typescript
// ✅ High overhead - CACHE IT
- Database queries: ~10-100ms
- External APIs: ~50-500ms  
- File I/O: ~5-50ms
- Complex algorithms: >5ms

// ❌ Low overhead - DON'T CACHE
- Property access: ~0.001ms
- Simple lookups: ~0.1ms
- Basic validation: ~1ms
- Type checks: ~0.01ms

// Example decision tree:
const expensiveOperation = async () => {
  return prisma.query(); // 50ms - CACHE IT
};

const cheapOperation = (data: any) => {
  return data.property; // 0.001ms - DON'T CACHE
};
```

### Avoid Cache Wrapper Anti-Pattern

```typescript
// ❌ Don't create wrapper functions just for caching
const getCachedUserPermissions = withCache(
  async (userId: string) => getUserPermissions(userId),
  { key: createCacheKey.user.permissions(userId), ttl: 3600 * 1000 } // 1 hour in milliseconds
);

// ✅ Add caching directly to the original function
export const getUserPermissions = withCache(
  async (userId: string) => {
    return prisma.user.findUnique({
      where: { id: userId },
      include: { permissions: true }
    });
  },
  { key: createCacheKey.user.permissions(userId), ttl: 3600 * 1000 } // 1 hour in milliseconds
);
```

## TTL Coordination Strategy

### Multi-Layer Cache Coordination
For endpoints serving client SDKs, coordinate TTLs across layers:

```typescript
// Client SDK cache (expiresAt) - longest TTL for fewer requests
const CLIENT_TTL = 60 * 60;           // 1 hour (seconds for client)

// Server Redis cache - shorter TTL ensures fresh data for clients  
const SERVER_TTL = 60 * 30 * 1000;   // 30 minutes in milliseconds

// HTTP cache headers (seconds)
const BROWSER_TTL = 60 * 60;         // 1 hour (max-age)
const CDN_TTL = 60 * 30;             // 30 minutes (s-maxage)
const CORS_TTL = 60 * 60;            // 1 hour (balanced approach)
```

### Standard TTL Guidelines (in milliseconds for cache-manager + Keyv)
```typescript
// Configuration data - rarely changes
const CONFIG_TTL = 60 * 60 * 24 * 1000;     // 24 hours

// User data - moderate frequency  
const USER_TTL = 60 * 60 * 2 * 1000;        // 2 hours

// Survey data - changes moderately
const SURVEY_TTL = 60 * 15 * 1000;          // 15 minutes

// Billing data - expensive to compute
const BILLING_TTL = 60 * 30 * 1000;         // 30 minutes

// Action classes - infrequent changes
const ACTION_CLASS_TTL = 60 * 30 * 1000;    // 30 minutes
```

## High-Frequency Endpoint Optimization

### Performance Patterns for High-Volume Endpoints

```typescript
// ✅ Optimized high-frequency endpoint pattern
export const GET = async (request: NextRequest, props: { params: Promise<{ id: string }> }) => {
  const params = await props.params;

  try {
    // Simple validation (avoid Zod for high-frequency)
    if (!params.id || typeof params.id !== 'string') {
      return responses.badRequestResponse("ID is required", undefined, true);
    }

    // Single optimized query with caching
    const data = await getOptimizedData(params.id);

    return responses.successResponse(
      {
        data,
        expiresAt: new Date(Date.now() + CLIENT_TTL * 1000), // SDK cache duration
      },
      true,
      "public, s-maxage=1800, max-age=3600, stale-while-revalidate=1800, stale-if-error=3600"
    );
  } catch (err) {
    // Simplified error handling for performance
    if (err instanceof ResourceNotFoundError) {
      return responses.notFoundResponse(err.resourceType, err.resourceId);
    }
    logger.error({ error: err, url: request.url }, "Error in high-frequency endpoint");
    return responses.internalServerErrorResponse(err.message, true);
  }
};
```

### Avoid These Performance Anti-Patterns

```typescript
// ❌ Avoid for high-frequency endpoints
const inputValidation = ZodSchema.safeParse(input);     // Too slow
const startTime = Date.now(); logger.debug(...);       // Logging overhead
const { data, revalidateEnvironment } = await get();   // Complex return types
```

### CORS Optimization
```typescript
// ✅ Balanced CORS caching (not too aggressive)
export const OPTIONS = async (): Promise<Response> => {
  return responses.successResponse(
    {},
    true,
    "public, s-maxage=3600, max-age=3600" // 1 hour balanced approach
  );
};
```

## Redis Cache Migration from Next.js

### Avoid Legacy Next.js Patterns
```typescript
// ❌ Old Next.js unstable_cache pattern (avoid)
const getCachedData = unstable_cache(
  async (id) => fetchData(id),
  ['cache-key'],
  { tags: ['environment'], revalidate: 900 }
);

// ❌ Don't use revalidateEnvironment flags with Redis
return { data, revalidateEnvironment: true }; // This gets cached incorrectly!

// ✅ New Redis pattern with withCache (TTL in milliseconds)
export const getCachedData = (id: string) => 
  withCache(
    () => fetchData(id),
    {
      key: createCacheKey.environment.data(id),
      ttl: 60 * 15 * 1000, // 15 minutes in milliseconds
    }
  )();
```

### Remove Revalidation Logic
When migrating from Next.js `unstable_cache`:
- Remove `revalidateEnvironment` or similar flags
- Remove tag-based invalidation logic  
- Use TTL-based expiration instead
- Handle one-time updates (like `appSetupCompleted`) directly in cache

## Data Layer Optimization

### Single Query Pattern
```typescript
// ✅ Optimize with single database query
export const getOptimizedEnvironmentData = async (environmentId: string) => {
  return prisma.environment.findUniqueOrThrow({
    where: { id: environmentId },
    include: {
      project: { 
        select: { id: true, recontactDays: true, /* ... */ }
      },
      organization: {
        select: { id: true, billing: true }
      },
      surveys: {
        where: { status: "inProgress" },
        select: { id: true, name: true, /* ... */ }
      },
      actionClasses: {
        select: { id: true, name: true, /* ... */ }
      }
    }
  });
};

// ❌ Avoid multiple separate queries
const environment = await getEnvironment(id);
const organization = await getOrganization(environment.organizationId);  
const surveys = await getSurveys(id);
const actionClasses = await getActionClasses(id);
```

## Invalidation Best Practices

**Always use explicit key-based invalidation:**

```typescript
// ✅ Clear and debuggable
await invalidateCache(createCacheKey.environment.state(environmentId));
await invalidateCache([
  createCacheKey.environment.surveys(environmentId),
  createCacheKey.environment.actionClasses(environmentId)
]);

// ❌ Avoid complex tag systems
await invalidateByTags(["environment", "survey"]); // Don't do this
```

## Critical Performance Targets

### High-Frequency Endpoint Goals
- **Cache hit ratio**: >85%
- **Response time P95**: <200ms  
- **Database load reduction**: >60%
- **HTTP cache duration**: 1hr browser, 30min Cloudflare
- **SDK refresh interval**: 1 hour with 30min server cache

### Performance Monitoring
- Use **existing elastic cache analytics** for metrics
- Log cache errors and warnings (not debug info)
- Track database query reduction
- Monitor response times for cached endpoints
- **Avoid performance logging** in high-frequency endpoints

## Error Handling Pattern

Always provide fallback to fresh data on cache errors:

```typescript
try {
  const cached = await cache.get(key);
  if (cached) return cached;
  
  const fresh = await fetchFresh();
  await cache.set(key, fresh, ttl); // ttl in milliseconds
  return fresh;
} catch (error) {
  // ✅ Always fallback to fresh data
  logger.warn("Cache error, fetching fresh", { key, error });
  return fetchFresh();
}
```

## Common Pitfalls to Avoid

1. **Never use Next.js `unstable_cache()`** - unreliable in production
2. **Don't use revalidation flags with Redis** - they get cached incorrectly
3. **Avoid Zod validation** for simple parameters in high-frequency endpoints
4. **Don't add performance logging** to high-frequency endpoints
5. **Coordinate TTLs** between client and server caches
6. **Don't over-engineer** with complex tag systems  
7. **Avoid caching rapidly changing data** (real-time metrics)
8. **Always validate cache keys** to prevent collisions
9. **Don't add redundant caching layers** - analyze computational overhead first
10. **Avoid cache wrapper functions** - add caching directly to expensive operations
11. **Don't cache property access or simple transformations** - overhead is negligible
12. **Analyze the full call chain** before adding caching to avoid double-caching
13. **Remember TTL is in milliseconds** for cache-manager + Keyv stack (not seconds)

## Monitoring Strategy

- Use **existing elastic cache analytics** for metrics
- Log cache errors and warnings  
- Track database query reduction
- Monitor response times for cached endpoints
- **Don't add custom metrics** that duplicate existing monitoring

## Important Notes

### TTL Units
- **cache-manager + Keyv**: TTL in **milliseconds**
- **Direct Redis commands**: TTL in **seconds** (EXPIRE, SETEX) or **milliseconds** (PEXPIRE, PSETEX)
- **HTTP cache headers**: TTL in **seconds** (max-age, s-maxage)
- **Client SDK**: TTL in **seconds** (expiresAt calculation)
