Performance Monitoring
The API includes built-in performance monitoring capabilities that provide insights into query execution time, cache usage, and database query counts. This guide explains how to use these features and how to interpret the performance data in API responses.
Overview
Performance metrics are provided through Server-Timing headers, which can be viewed in browser developer tools or by using the -v flag with curl. This approach provides performance insights without cluttering the JSON response body.
The API uses Redis (via Dragonfly) for caching frequently accessed data, which significantly improves performance by reducing database load and response times.
Redis Caching
The API uses Redis (via Dragonfly) for caching frequently accessed data. This significantly improves performance by reducing database load and response times. The caching system is integrated with the performance monitoring system to provide insights into cache hits and misses through Server-Timing headers.
Cache Configuration
Redis caching is configured in the .env file with the following settings:
REDIS_HOST=dragonfly
REDIS_PORT=6379
REDIS_USERNAME=
REDIS_PASSWORD=your_password
REDIS_CLUSTER_MODE=false
REDIS_CACHE_TTL=3600
REDIS_TLS_ENABLED=false
REDIS_TLS_CERT_REQS=none
REDIS_TLS_CA_CERTS_FILE=
REDIS_TLS_CERTFILE=
REDIS_TLS_KEYFILE=
In production, the system is configured to use AWS ValKey (a Redis-compatible service) in cluster mode with TLS enabled for secure communication.
TLS Configuration for Redis
When using AWS ValKey in production, TLS is enabled to secure the connection. The following environment variables control TLS behavior:
REDIS_TLS_ENABLED: Set totrueto enable TLS for Redis connectionsREDIS_TLS_CERT_REQS: Certificate verification mode (none,optional, orrequired)REDIS_TLS_CA_CERTS_FILE: Path to CA certificate file for verifying the server's certificateREDIS_TLS_CERTFILE: Path to client certificate file (optional)REDIS_TLS_KEYFILE: Path to client key file (optional)
For most AWS ValKey deployments, you'll only need to set REDIS_TLS_ENABLED=true and possibly provide a CA certificate file. Client certificates are typically not required.
Dragonfly Redis Implementation
Dragonfly is a modern in-memory datastore, fully compatible with Redis and Memcached APIs but with better performance and memory efficiency. In our API, we use Dragonfly as a drop-in replacement for Redis to provide high-performance caching.
Key benefits of using Dragonfly:
- Performance: Dragonfly offers significantly better performance than Redis for most operations.
- Memory Efficiency: Dragonfly uses less memory than Redis for the same dataset.
- Compatibility: Dragonfly is fully compatible with the Redis API, making it easy to switch between them.
- Simplicity: Dragonfly is easy to set up and use, with minimal configuration required.
The Dragonfly container is configured in the compose.yml file:
dragonfly:
image: docker.dragonflydb.io/dragonflydb/dragonfly
command: --maxmemory 6gb --requirepass ${REDIS_PASSWORD?REQUIRED}
ulimits:
memlock: -1
volumes:
- dragonfly_data:/data
networks:
- tracker-network
restart: unless-stopped
This setup provides a Redis-compatible service that can be used by the API for caching without any changes to the code.
How Caching Works
The API uses a generic caching system that can cache any type of data. The caching system is implemented in the app/core/redis.py file and provides the following features:
- Automatic serialization/deserialization: The cache manager automatically serializes and deserializes data to and from JSON using the high-performance
orjsonlibrary. - Type safety: The cache manager is generic and type-safe, ensuring that the cached data is of the expected type.
- Cache invalidation: The cache manager provides methods to invalidate specific cache keys or all keys with a specific prefix.
- Performance monitoring integration: The cache manager automatically updates the performance metrics when a cache hit occurs.
- Special data type handling: The cache manager includes a custom
orjson_defaultfunction that properly handles datetime objects, ensuring they can be serialized to and deserialized from JSON correctly.
Using orjson for Serialization
The API uses the orjson library for JSON serialization and deserialization, which offers several advantages over the standard json module:
- Performance:
orjsonis significantly faster than the standardjsonmodule, especially for large datasets. - Memory efficiency:
orjsonuses less memory than the standardjsonmodule. - Feature-rich:
orjsonsupports more data types out of the box, including datetime objects, UUID, and more. - Strict validation:
orjsonperforms stricter validation of JSON data, helping to catch errors early.
The orjson library is used throughout the caching system to ensure optimal performance when serializing and deserializing data to and from Redis.
Using the Cache in Route Handlers
Route handlers can use the cache manager to cache responses and check for cached data. Here's an example:
@router.get("/{item_id}", response_model=schemas.Item)
def read_item(
*,
request: Request,
db: Session = Depends(deps.get_db),
item_id: int,
current_user: User = Depends(deps.get_current_active_user),
cache_manager: deps.CacheManager = Depends(lambda: deps.get_cache_manager(schemas.Item)),
) -> Any:
# Generate cache key
cache_key = f"item:{item_id}:user:{current_user.id}"
# Try to get from cache first
cached_data = cache_manager.get(cache_key, request)
if cached_data:
# Add performance headers
request.state.query_time = round((time.time() - start_time) * 1000, 2)
request.state.cache_status = "hit"
request.state.query_count = 0
return cached_data
# If not in cache, get from database
item = crud.item.get(db, id=item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
# Add performance headers
request.state.query_time = round((time.time() - start_time) * 1000, 2)
request.state.cache_status = "miss"
request.state.query_count = 1
# Cache the item
cache_manager.set(cache_key, item)
return item
Cache Invalidation
When data is modified, the cache needs to be invalidated to ensure that users see the most up-to-date information. The API automatically invalidates the cache when data is modified through the API endpoints.
For example, when a brand is updated, the cache for that brand and all brand lists is invalidated:
@router.put("/{brand_id}", response_model=schemas.Brand)
def update_brand(
*,
db: Session = Depends(deps.get_db),
brand_id: int,
brand_in: schemas.BrandUpdate,
current_user: User = Depends(deps.get_current_active_user),
brand_cache: deps.CacheManager = Depends(lambda: deps.get_cache_manager(schemas.Brand)),
brands_cache: deps.CacheManager = Depends(lambda: deps.get_cache_manager(List[schemas.Brand])),
) -> Any:
# Update the brand
brand = crud.brand.update(db, db_obj=brand, obj_in=brand_in)
# Invalidate cache for this brand and all brand lists
brand_cache.invalidate_by_prefix(f"brand:{brand_id}")
brands_cache.invalidate_by_prefix("brands:")
return brand
Caching Strategy
For comprehensive caching strategy including cache key standards, TTL tiers, and invalidation patterns, see Caching System.
The caching system addresses your specific issues:
- Map location stale data: 15-minute TTL with immediate invalidation on location updates
- Consistent cache keys: Standardized format across all entities
- Admin panel updates: Automatic cache clearing when data is modified
- Frontend alignment: Identical TTL values between React Query and Redis
Performance Metrics
Performance metrics are now provided exclusively through Server-Timing headers rather than in the JSON response body. This approach keeps the API responses clean and focused on the data, while still providing valuable performance insights through the headers.
You can view these metrics by using the -v flag with curl:
curl -v -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8000/api/v1/brands/
This will show the Server-Timing header in the response:
< server-timing: db;dur=3.56;desc="Database Query Time", cache;desc="Cache Hit", queries;desc="0 Queries"
Note the significant performance improvement when the response is served from cache - the query time is reduced from around 40ms to less than 4ms, and no database queries are executed.
Server-Timing Headers
The API includes Server-Timing headers in all responses, which can be viewed in browser developer tools. These headers provide the same performance metrics as the JSON response, but in a format that can be visualized in the browser's network panel.
Example Server-Timing headers:
For a cache miss:
Server-Timing: db;dur=42.56;desc="Database Query Time", cache;desc="Cache Miss", queries;desc="3 Queries"
For a cache hit:
Server-Timing: db;dur=0.37;desc="Database Query Time", cache;desc="Cache Hit", queries;desc="0 Queries"
To view these headers in your browser:
- Open the browser's developer tools (F12 or Ctrl+Shift+I)
- Go to the Network tab
- Make a request to the API
- Select the request and look at the Timing tab
- The Server-Timing metrics will be displayed in the timing breakdown
Available Metrics
The following performance metrics are included in API responses:
- query_time_ms: The total time taken to process the request, in milliseconds.
- cache_status: Indicates whether the response was served from cache ("hit") or required a database query ("miss").
- query_count: The number of database queries executed to fulfill the request.
Enabling Performance Monitoring
Performance monitoring is enabled by default in the API. The PerformanceMiddleware is automatically applied to all requests and adds the performance metrics to JSON responses.
Using Performance Monitoring in Route Handlers
Route handlers can interact with the performance monitoring system to provide more accurate metrics. For example, you can manually update the cache status or increment the query count.
Tracking Cache Status
from fastapi import Request
from app.core.performance import update_cache_status
@router.get("/items")
def read_items(request: Request):
# Check if data is coming from cache
cache_used = check_cache() # Your cache checking logic
# Update the cache status
update_cache_status(request, cache_used)
# Return the data
return data
Tracking Query Count
from fastapi import Request
from app.core.performance import increment_query_count
@router.get("/items")
def read_items(request: Request):
# Increment the query count when executing a database query
increment_query_count(request)
# Execute the query
items = db.query(Item).all()
# Return the data
return items
Debugging with FastAPI
The API has debugging enabled, which provides more detailed error information in the browser. This is particularly useful during development to diagnose issues.
When an error occurs, the API will return a detailed error response that includes:
- The error message
- The stack trace
- Request information
This debugging information is only available when running the API with debug=True (which is the default configuration).
Performance Considerations
While performance monitoring provides valuable insights, it does add a small overhead to request processing. In production environments with high traffic, you may want to consider disabling or optimizing the performance monitoring system.
Best Practices
- Use cache where appropriate: For frequently accessed data that doesn't change often, consider implementing caching to improve performance.
- Minimize database queries: Try to minimize the number of database queries per request by using efficient query patterns like joins and bulk operations.
- Monitor performance trends: Regularly review performance metrics to identify slow endpoints or queries that could benefit from optimization.
- Set performance budgets: Establish performance budgets for critical endpoints and monitor them to ensure they stay within acceptable limits.