Skip to main content

Overview

Azen automatically tracks all API usage for monitoring, billing, and analytics. Usage is aggregated per organization, API key, and route, with detailed metrics stored in Redis and Postgres.

Usage Metrics

Tracked Operations

Azen tracks the following metrics for each API request:
  • totalRequests: Total number of API requests
  • successCount: Successful requests (HTTP 2xx, 3xx)
  • errorCount: Failed requests (HTTP 4xx, 5xx)
  • memoryCount: Number of memories created (POST /memory)
  • searchCount: Number of search queries (POST /memory/search)

Aggregation Levels

Metrics are aggregated by:
  • Organization: All usage for your organization
  • API Key: Usage per individual API key
  • Date: Daily granularity (YYYY-MM-DD format)
  • Route Group: memory_create or memory_search

Retrieving Usage Statistics

Get Usage Data

curl -X GET https://api.azen.sh/api/v1/usage \
  -H "azen-api-key: YOUR_API_KEY"

Response Format

Response (200 OK):
{
  "range": {
    "start": "2024-01-01",
    "end": "2024-01-30"
  },
  "labels": [
    "2024-01-01",
    "2024-01-02",
    "2024-01-03",
    // ... 30 days
  ],
  "series": {
    "memory_create": [10, 15, 8, ...],
    "memory_search": [25, 30, 22, ...],
    "total": [35, 45, 30, ...],
    "success": [33, 43, 29, ...],
    "error": [2, 2, 1, ...]
  },
  "summary": {
    "total_requests": 1250,
    "success_count": 1200,
    "error_count": 50,
    "memory_count": 450,
    "search_count": 800
  }
}

Response Fields

range

  • start: First day of the 30-day period (YYYY-MM-DD)
  • end: Last day (today)

labels

Array of 30 date strings (one per day) in chronological order.

series

Daily time series data aligned with labels:
  • memory_create: Memories created per day
  • memory_search: Search queries per day
  • total: Total requests per day
  • success: Successful requests per day
  • error: Failed requests per day

summary

Aggregated totals across the entire 30-day period.
The usage endpoint returns the last 30 days of data. Historical data beyond 30 days is preserved in the database but not returned by the API.

How Usage Tracking Works

Request Tracking Flow

1

Request enters middleware

The trackUsage middleware intercepts requests to /api/v1/memory and /api/v1/memory/search.
2

Extract context

Middleware extracts userId, apiKeyId, organizationId from the authenticated request.
3

Determine route group

Request is classified:
  • POST /api/v1/memorymemory_create
  • POST /api/v1/memory/searchmemory_search
4

Execute request

The actual API operation runs. Success/failure is tracked based on HTTP status.
5

Increment Redis counters

Metrics are incremented in Redis using atomic HINCRBY operations.
6

Background sync

A background worker syncs Redis metrics to Postgres for persistence and analytics.

Implementation Reference

From apps/api/src/middlewares/trackUsage.ts:
export const trackUsage = createMiddleware(async (c, next) => {
  const userId = c.get("userId");
  const apiKeyId = c.get("apiKeyId");
  const organizationId = c.get("organizationId");

  // Determine route group
  let routeGroup: string | null = null;
  let perRouteField: "memoryCount" | "searchCount" | null = null;

  if (method === "POST" && path === "/api/v1/memory") {
    routeGroup = "memory_create";
    perRouteField = "memoryCount";
  } else if (method === "POST" && path === "/api/v1/memory/search") {
    routeGroup = "memory_search";
    perRouteField = "searchCount";
  }

  // Track success/failure
  let success = true;
  try {
    await next();
    if ((c.res.status ?? 200) >= 400) success = false;
  } catch (err) {
    success = false;
    throw err;
  } finally {
    // Increment Redis counters
    const date = new Date().toISOString().slice(0, 10);
    const key = `usage:${organizationId}:${userId}:${apiKeyId}:${date}:${routeGroup}`;

    const pipeline = redis.pipeline();
    pipeline.hincrby(key, "totalRequests", 1);
    pipeline.hincrby(key, success ? "successCount" : "errorCount", 1);
    
    if (success && perRouteField === "memoryCount") {
      pipeline.hincrby(key, "memoryCount", 1);
    }
    
    await pipeline.exec();
  }
});

Data Storage

Redis (Real-time)

  • Purpose: Fast, atomic counters for live metrics
  • TTL: 7 days (metrics automatically expire)
  • Structure: Hash keys with counters per metric
Example Redis key:
usage:org_abc:user_123:key_456:2024-01-15:memory_create
Hash fields:
totalRequests: 150
successCount: 148
errorCount: 2
memoryCount: 148

Postgres (Long-term)

  • Purpose: Persistent storage for analytics and billing
  • Table: api_usage
  • Granularity: Per organization, API key, date, and route group
Schema (from packages/db/src/db/schema.ts):
export const apikeyUsage = pgTable("api_usage", {
  id: text("id").primaryKey().notNull(),
  userId: text("user_id").notNull(),
  organizationId: text("organization_id"),
  apiKeyId: text("api_key_id").notNull(),
  date: text("date").notNull(),
  routeGroup: text("route_group").notNull(),
  totalRequests: integer("total_requests").notNull().default(0),
  successCount: integer("success_count").notNull().default(0),
  errorCount: integer("error_count").notNull().default(0),
  memoryCount: integer("memory_count").notNull().default(0),
  searchCount: integer("search_count").notNull().default(0),
  createdAt: timestamp("created_at").defaultNow(),
  updatedAt: timestamp("updated_at").defaultNow(),
});

Monitoring Usage

Dashboard Visualization

The console dashboard displays usage metrics as:
  • Time series charts: Daily request trends over 30 days
  • Summary cards: Total requests, memories, searches
  • Success rate: Percentage of successful vs. failed requests
  • Per-key breakdown: Usage by individual API key

Query Historical Data

For custom analytics, query the api_usage table directly:
SELECT 
  date,
  SUM(total_requests) AS total_requests,
  SUM(memory_count) AS memories_created,
  SUM(search_count) AS searches_performed
FROM api_usage
WHERE organization_id = 'org_abc'
  AND date >= '2024-01-01'
GROUP BY date
ORDER BY date ASC;

Usage Alerts

High Error Rate

Monitor errorCount vs. successCount. An error rate above 5% may indicate:
  • Invalid authentication
  • Rate limiting issues
  • Malformed requests
  • Service degradation

Unexpected Spikes

Sudden increases in totalRequests could signal:
  • Legitimate growth
  • Runaway processes (e.g., infinite loops)
  • DDoS or abuse
Set up alerts in your monitoring system to notify you when error rates or request volumes exceed thresholds.

Rate Limiting Impact

Usage tracking is separate from rate limiting, but they interact:
  • Rate limits are enforced per API key in real-time
  • Usage tracking records all requests, including rate-limited ones
  • Error count increments when requests are rate-limited (HTTP 429)
See Rate Limits for details on rate limit configuration.

Privacy and Data Retention

What’s Tracked

  • Request counts and timestamps
  • Success/failure status
  • Organization, user, and API key IDs
  • Route groups (operation types)

What’s NOT Tracked

  • Request payloads (memory content)
  • Response bodies
  • User IP addresses
  • Query parameters
Azen does not log or store memory content in usage metrics. All tracking is aggregated and anonymized.

Data Retention

  • Redis: 7 days (automatic expiration)
  • Postgres: Indefinite (for billing and analytics)
  • Console: Last 30 days displayed by default

Troubleshooting

Missing Usage Data

Symptom: Usage endpoint returns zeros or empty series Possible causes:
  • No API requests have been made in the last 30 days
  • Requests were made with a different organization’s API key
  • Redis or Postgres sync failed
Solution: Verify requests are being sent and check sync worker logs.

Incorrect Counts

Symptom: Usage counts don’t match expected values Possible causes:
  • Failed requests are counted in errorCount, not in memoryCount/searchCount
  • Multiple API keys in the same organization (check per-key breakdown)
  • Background sync delay (Redis → Postgres)
Solution: Wait a few minutes for sync, or query Redis directly for real-time counts.

Background Sync Worker

Usage data flows from Redis to Postgres via a background worker: From apps/api/src/workers/syncWorker.ts:
// Sync worker runs periodically
setInterval(async () => {
  const keys = await redis.smembers(KEYS_SET);
  
  for (const key of keys) {
    const data = await redis.hgetall(key);
    
    // Parse key components
    const [_, orgId, userId, apiKeyId, date, routeGroup] = key.split(':');
    
    // Upsert to Postgres
    await db.insert(apikeyUsage).values({
      id: randomUUID(),
      organizationId: orgId,
      userId,
      apiKeyId,
      date,
      routeGroup,
      totalRequests: parseInt(data.totalRequests),
      successCount: parseInt(data.successCount),
      errorCount: parseInt(data.errorCount),
      memoryCount: parseInt(data.memoryCount || '0'),
      searchCount: parseInt(data.searchCount || '0'),
    }).onConflictDoUpdate({
      target: [apikeyUsage.organizationId, apikeyUsage.apiKeyId, apikeyUsage.date, apikeyUsage.routeGroup],
      set: {
        totalRequests: sql`api_usage.total_requests + ${parseInt(data.totalRequests)}`,
        successCount: sql`api_usage.success_count + ${parseInt(data.successCount)}`,
        // ...
      }
    });
  }
}, 60000); // Run every 60 seconds
The sync worker runs every 60 seconds, so there may be a slight delay between Redis and Postgres data.

Next Steps

Rate Limits

Understand how rate limiting affects usage

Managing API Keys

Configure rate limits for your API keys

Organizations

Learn about organization-level usage aggregation

API Reference

Explore all API endpoints

Build docs developers (and LLMs) love