This guide explains the comprehensive performance monitoring and optimization system implemented in AgenticPay.
- Core Web Vitals Tracking
- API Response Compression
- Cursor-Based Pagination
- Database Connection Pooling
- Redis Caching Layer
- Monitoring Dashboard
- Performance Budgets
Core Web Vitals are critical metrics that measure user experience:
- LCP (Largest Contentful Paint): Time for the largest content element to render (target: <2.5s)
- FID (First Input Delay): Time from first user interaction to browser response (target: <100ms)
- CLS (Cumulative Layout Shift): Measure of visual stability (target: <0.1)
- TTFB (Time to First Byte): Time for first response (target: <600ms)
- FCP (First Contentful Paint): Time for first content to appear (target: <1.8s)
Located in: frontend/lib/performance.ts
import { performanceMonitor } from '@/lib/performance';
// Automatically initialized on page load
// Tracks all Core Web Vitals and sends to Sentry- Real-time metric collection using
PerformanceObserver - Automatic reporting on page hide/unload
- Sentry integration for RUM
- Analytics API endpoint for custom tracking
Track navigation performance between routes:
performanceMonitor.trackRouteTransition('/dashboard', '/payments');Reduces payload size by 60-80% on average, improving:
- Network transfer time
- Bandwidth costs
- Mobile performance
-
Brotli (preferred): 20-30% smaller than gzip
- Quality level: 5 (balanced speed/compression)
- Mode: Text optimization
-
Gzip (fallback): Universal support
- Compression level: 6
- Minimum size threshold: 1KB
Located in: backend/src/middleware/compression.ts
app.use(compressionMiddleware({
brotliLevel: 5,
gzipLevel: 6,
minSizeBytes: 1024,
}));Access compression metrics via:
GET /api/v1/monitoring/pool/compression
Returns:
{
"totalRequests": 1000,
"compressedRequests": 950,
"totalOriginalSize": 52428800,
"totalCompressedSize": 10485760,
"compressionRatio": 80.0,
"brotliRequests": 600,
"gzipRequests": 350
}Efficient pagination for large datasets using cursor-based approach instead of offset:
Benefits:
- O(1) query performance regardless of page position
- Handles data mutations between requests
- Smaller payloads with field selection
# Get first 20 items
GET /api/v1/payments?limit=20
# Get next page
GET /api/v1/payments?cursor=<endCursor>&limit=20
# Select specific fields
GET /api/v1/payments?limit=20&fields=id,amount,status
# With conditional requests
GET /api/v1/payments -H "If-None-Match: <etag>"{
"data": [...],
"pageInfo": {
"startCursor": "base64_encoded_id",
"endCursor": "base64_encoded_id",
"hasNextPage": true,
"hasPreviousPage": false,
"totalCount": 1000,
"pageSize": 20
},
"_meta": {
"requestId": "req-123",
"timestamp": "2024-01-01T00:00:00Z",
"cacheStatus": "HIT"
}
}Located in: backend/src/middleware/pagination.ts
import { paginationMiddleware, CursorPaginator } from './pagination';
// Apply middleware
app.use(paginationMiddleware);
// Use in routes
router.get('/items', async (req, res) => {
const items = await db.items.findMany({
take: req.pagination.limit,
skip: req.pagination.cursor ? 1 : 0,
cursor: req.pagination.cursor ? { id: CursorPaginator.decodeCursor(req.pagination.cursor) } : undefined,
});
res.sendPaginated(items, totalCount, cacheStatus);
});Automatic ETag generation for cache validation:
GET /api/v1/payments
ETag: "abc123def456"
GET /api/v1/payments -H "If-None-Match: abc123def456"
→ 304 Not Modified (no body sent)
Optimized connection pooling with PgBouncer for efficient resource utilization:
Benefits:
- Prevents connection exhaustion
- Detects and prevents connection leaks
- Monitors pool health in real-time
- Automatic alerting on degradation
Located in: backend/src/config/database.ts
Production settings:
{
max: 50, // Max connections
min: 5, // Min connections
acquireTimeoutMs: 10000, // Timeout for acquiring connection
idleTimeoutMs: 300000, // Timeout for idle connections (5 min)
maxConnectionAgeMs: 1800000 // Max age (30 min)
}Located in: backend/src/config/database.ts
{
poolMode: "transaction", // Per-transaction pooling
defaultPoolSize: 25,
maxPoolSize: 50,
reservePoolSize: 5,
queryTimeoutMs: 30000,
serverLifetimeMs: 3600000 // Server lifetime (1 hour)
}Access pool health via:
GET /api/v1/monitoring/pool/health
Returns:
{
"status": "healthy",
"activeConnections": 25,
"idleConnections": 10,
"utilizationPercent": 50,
"poolSize": { "min": 5, "max": 50 },
"leaks": { "detected": 0, "threshold": 5 },
"exhaustion": { "events": 0 },
"recommendations": ["Pool operating normally"]
}Automatic detection of connection leaks:
GET /api/v1/monitoring/pool/leaks
- Monitors connection acquisition/release
- Alerts on connections held longer than threshold
- Automatic cleanup after timeout
GET /api/v1/monitoring/pool/metrics
Returns comprehensive pool statistics including:
- Connection counts (active, idle, waiting)
- Lease statistics (total, active, released, errors)
- Peak connection usage
- Average acquire time
Intelligent caching system with automatic invalidation for high-performance data access:
Benefits:
- 90%+ cache hit rate for hot data
- Sub-millisecond response times
- Event-driven automatic invalidation
- Cache warming on startup
- Real-time hit rate metrics
Located in: backend/src/services/cache.ts
import { getCacheService } from '@/services/cache';
const cache = await getCacheService();
// Get with fallback loader
const user = await cache.get('user:123',
() => db.users.findUnique({ where: { id: '123' } }),
300 // 5-minute TTL
);
// Direct set
await cache.set('user:123', userData, 300);
// Delete
await cache.delete('user:123');
// Invalidate by pattern
await cache.invalidateKeys(['user:*', 'dashboard:*']);
// Get metrics
const metrics = cache.getMetrics();
console.log(`Hit rate: ${metrics.hitRate.toFixed(2)}%`);Events automatically invalidate related cache:
// Payment events
'payment.created' → invalidates ['payments:list', 'dashboard:overview']
'payment.completed' → invalidates ['payments:list', 'analytics:*']
// Invoice events
'invoice.paid' → invalidates ['invoices:list', 'dashboard:overview']
// User events
'user.updated' → invalidates ['user:*', 'dashboard:*']Critical data preloaded on startup:
cache.registerWarmer('dashboard:overview',
() => db.dashboards.getOverview(),
3600 // 1 hour TTL
);
// Warming triggered during initializationGET /api/v1/monitoring/pool/cache
Returns:
{
"hits": 1000,
"misses": 100,
"hitRate": 90.9,
"sets": 150,
"deletes": 50,
"errors": 2,
"avgSizeBytes": 2048
}Comprehensive view of all performance metrics:
GET /api/v1/monitoring/pool/performance
Returns combined metrics:
- Performance score (0-100)
- Pool health status
- Compression ratio
- Cache hit rate
- Recommendations
All Core Web Vitals are automatically sent to Sentry:
GET https://sentry.io/organizations/agenticpay/
→ agenticpay-frontend project
→ Performance tab
→ Core Web Vitals section
Performance checks run on every commit:
GitHub Actions → .github/workflows/performance-monitoring.yml
- Bundle Size: < 5MB total
- Core Web Vitals: Within thresholds
- Lighthouse Score: > 90
- Compression Ratio: > 70%
- Cache Hit Rate: > 80%
- Pool Health: No exhaustion events
# Analyze bundle
npm run analyze:bundle
# Run Lighthouse locally
npm run lighthouse:ci
# Check performance metrics
npm run test:performance- Use cursor pagination for list endpoints
- Leverage cache service for frequently accessed data
- Monitor pool health before deploying
- Validate compression is enabled for all text responses
- Import performance monitor to track custom metrics
- Use pagination helper for large data lists
- Minimize JavaScript and defer non-critical code
- Optimize images with Next.js Image component
- Monitor pool exhaustion alerts in production
- Tune pool size based on actual usage patterns
- Configure PgBouncer maintenance windows
- Scale Redis horizontally when hit rate drops
- Check invalidation rules are correct
- Verify cache warming is running
- Increase TTL for stable data
- Check Redis memory usage
- Reduce query time with proper indexing
- Increase pool size gradually
- Check for connection leaks
- Monitor slow queries
- Verify Brotli is supported by clients
- Check minimum size threshold
- Increase compression level (trade-off: speed)
- Identify uncompressible content types