Skip to content

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 to true to enable TLS for Redis connections
  • REDIS_TLS_CERT_REQS: Certificate verification mode (none, optional, or required)
  • REDIS_TLS_CA_CERTS_FILE: Path to CA certificate file for verifying the server's certificate
  • REDIS_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:

  1. Performance: Dragonfly offers significantly better performance than Redis for most operations.
  2. Memory Efficiency: Dragonfly uses less memory than Redis for the same dataset.
  3. Compatibility: Dragonfly is fully compatible with the Redis API, making it easy to switch between them.
  4. 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:

  1. Automatic serialization/deserialization: The cache manager automatically serializes and deserializes data to and from JSON using the high-performance orjson library.
  2. Type safety: The cache manager is generic and type-safe, ensuring that the cached data is of the expected type.
  3. Cache invalidation: The cache manager provides methods to invalidate specific cache keys or all keys with a specific prefix.
  4. Performance monitoring integration: The cache manager automatically updates the performance metrics when a cache hit occurs.
  5. Special data type handling: The cache manager includes a custom orjson_default function 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:

  1. Performance: orjson is significantly faster than the standard json module, especially for large datasets.
  2. Memory efficiency: orjson uses less memory than the standard json module.
  3. Feature-rich: orjson supports more data types out of the box, including datetime objects, UUID, and more.
  4. Strict validation: orjson performs 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:

  1. Open the browser's developer tools (F12 or Ctrl+Shift+I)
  2. Go to the Network tab
  3. Make a request to the API
  4. Select the request and look at the Timing tab
  5. 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

  1. Use cache where appropriate: For frequently accessed data that doesn't change often, consider implementing caching to improve performance.
  2. Minimize database queries: Try to minimize the number of database queries per request by using efficient query patterns like joins and bulk operations.
  3. Monitor performance trends: Regularly review performance metrics to identify slow endpoints or queries that could benefit from optimization.
  4. Set performance budgets: Establish performance budgets for critical endpoints and monitor them to ensure they stay within acceptable limits.