what's the best way to meter livekit sessions, so ...
# ask-ai
n
what's the best way to meter livekit sessions, so that we can know usage per member and limit access to an allotted amount of time per user
t
Thanks for reaching out, William. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — this is a great question. I’m responding to your request about how to meter LiveKit sessions so you can both (A) know usage per member and (B) enforce an allotted amount of time per user. High-level approach (what works best) • Use LiveKit’s connection minutes as the primary metering unit. Connection minutes measure participant minutes for human (standard) and agent participants and are the billing/usage metric you’ll want to track. • Combine LiveKit-side observability with application-side tracking. LiveKit shows overall usage and plan allotments in the dashboard, but to enforce per-user limits you should record join/leave times in your application. Concrete steps to implement per-member metering and limits 1. Record join and leave events in your app ◦ When a participant connects to a room, record timestamp + user ID. ◦ When they disconnect, record timestamp + user ID. ◦ Store ongoing sessions (in case of abrupt disconnects) and reconcile with periodic heartbeats or presence pings. 1. Calculate connection minutes per user ◦ For each session: round or sum the difference between join and leave times to minutes. ◦ Keep a running total per user (e.g., daily / monthly / per-plan window) so you can compare against allotments. 1. Enforce time limits at the application layer ◦ Before issuing a token or allowing join, check the user’s remaining minutes. ◦ If remaining minutes = 0, deny join or present a UI flow to buy more time. ◦ If remaining minutes < session length, allow join but enforce early disconnect when limit reached (see step 4). 1. Enforce disconnects when limits are reached ◦ Options: ◦ Server-initiated: use the server to revoke the token or call the RoomService API to remove the participant when their allotment is exhausted. ◦ Client-initiated: send a “time-up” message to the client and have it disconnect gracefully (useful for nicer UX). 1. Use dashboard / alerts for plan-level monitoring ◦ Monitor plan allotments and connection minute usage in the LiveKit dashboard to see aggregate consumption and set alerts when you approach limits. 1. Special handling for AI agents or recordings ◦ For AI agent sessions, use agent observability events and session recordings to get detailed, time-based tracking if needed. Design considerations and best practices • Decide your rounding strategy early (per-second vs per-minute). Billing uses connection minutes, so aligning your metering to minutes simplifies reconciliation. • Handle unexpected disconnects by preserving the join timestamp and reconciling on reconnect. • Consider soft limits (warning + grace period) to improve user experience and reduce churn. • Log all metering events for audit and billing reconciliation. Questions to help me give a tighter implementation plan • Do you plan to enforce limits in real time (disconnect immediately) or with soft warnings? • Are you using LiveKit Cloud (dashboard and plan allotments) or a self-hosted LiveKit instance? • Do you need code examples (server or client) for recording join/leave, calculating minutes, or disconnecting participants? If you tell me which enforcement style you prefer and whether you’re on LiveKit Cloud or self-hosted, I’ll provide a focused implementation checklist and code snippets you can drop into your backend. Sources: Understanding and Estimating Pricing for Video Conference and Livestream Us... | Towards a future-aligned pricing model | LiveKit | How much can LiveKit scale? | LiveKit | Pricing | LiveKit | Pricing | LiveKit, with interactive elements
n
are connection minutes available per user via api? is it available per day or week?
t
Thanks — I’m replying to your question about whether connection minutes are available per user via API, and whether you can get them by day or week. What’s available now • You can view per-participant (per-user) connection minutes and other participant-level telemetry in the LiveKit Cloud analytics/dashboard, and you can filter those analytics by custom time ranges. • LiveKit exposes usage data via Metrics Export APIs for external querying and aggregation. What’s not explicitly exposed • There isn’t a confirmed single API endpoint that directly returns “connection minutes per user by day/week.” The dashboard shows per-user metrics and the Metrics Export APIs provide usage data you can query, but you’ll typically need to export or aggregate the raw metrics to produce per-user daily/weekly connection-minute reports. Recommended approaches you can use right away • Application-side tracking (recommended for real-time enforcement) ◦ Record participant join and leave timestamps in your backend when you issue tokens or when you observe RoomService events. ◦ Aggregate those timestamps into minutes per user per day/week for reporting and enforcing limits in real time. • Metrics Export + external aggregation ◦ Export usage metrics from LiveKit (Metrics Export API) and run scheduled jobs to aggregate per-user connection minutes into daily/weekly buckets. This is good for historical reporting and reconciliations. • Hybrid ◦ Use app-side tracking for live enforcement (deny joins or forcibly disconnect when a limit is reached). ◦ Use exported metrics to reconcile billing and for analytics dashboards. If you want, I can: • Provide a short server-side pattern (with pseudocode) to record join/leave and compute per-user daily/weekly minutes. • Outline how to use Metrics Export data to compute per-user aggregates. • Recommend the best enforcement flow (immediate server disconnect vs. client-side soft warning) depending on your UX preference. Which of the above would you like next — a realtime enforcement code pattern or the export-and-aggregate approach? Sources: Understanding and Estimating Pricing for Video Conference and Livestream Us... | Say hello to improved Cloud analytics | LiveKit | Say hello to improved Cloud analytics | LiveKit | Announcing LiveKit Cloud | LiveKit | Pricing | LiveKit, with interactive elements
n
give the code for the livekit api reconciliation
t
Thanks — I’m responding to your request for code that reconciles LiveKit session usage into per-user connection minutes (daily/weekly). Below I provide a concise, practical pattern you can adapt. It combines two sources of truth: • Server-side join/leave events you record in real time (recommended for enforcement). • Periodic reconciliation with LiveKit-exported metrics (Metrics Export API) for auditing and billing reconciliation. I’ll give a Node.js example using Express + PostgreSQL for event storage, plus a reconciliation job that ingests exported metrics and computes per-user daily/weekly connection minutes. Adapt paths, table names, and auth to your stack. 1) Data model (Postgres) • sessions table: stores raw join/leave events and reconciled minutes. SQL (create tables):
Copy code
CREATE TABLE participant_events (
  id bigserial PRIMARY KEY,
  user_id text NOT NULL,
  room_id text NOT NULL,
  event_type text NOT NULL,   -- 'join' or 'leave'
  event_ts timestamptz NOT NULL DEFAULT now(),
  livekit_participant_sid text
);

CREATE TABLE reconciled_minutes (
  id bigserial PRIMARY KEY,
  user_id text NOT NULL,
  period_start date NOT NULL,  -- day-start or week-start
  period_type text NOT NULL,   -- 'day' or 'week'
  minutes integer NOT NULL,
  source text NOT NULL,        -- 'app' | 'metrics_export'
  reconciled_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE(user_id, period_start, period_type, source)
);
2) Recording join/leave events (server-side) • Record events when issuing tokens, and also subscribe to RoomService events or client webhooks to ensure correctness. Node.js (Express) example to record events:
Copy code
// POST /participant-event
// body: { user_id, room_id, event_type, livekit_participant_sid, event_ts? }
app.post('/participant-event', async (req, res) => {
  const { user_id, room_id, event_type, livekit_participant_sid, event_ts } = req.body;
  const ts = event_ts ? new Date(event_ts) : new Date();
  await db.query(
    `INSERT INTO participant_events (user_id, room_id, event_type, event_ts, livekit_participant_sid)
     VALUES ($1, $2, $3, $4, $5)`,
    [user_id, room_id, event_type, ts.toISOString(), livekit_participant_sid || null]
  );
  res.sendStatus(201);
});
Notes: • If you can, record both server-observed joins (when you mint tokens) and actual join events from RoomService or client pings for reliability. • For abrupt disconnects, include heartbeats or presence pings and mark a leave when heartbeat expires. 3) Sessionization: turn joins/leaves into session intervals • A job pairs join -> leave for each participant in a room and writes minutes per day/week to reconciled_minutes (source = 'app'). Node.js reconciliation logic (app events):
Copy code
async function processAppEventsForPeriod(periodStartDate) {
  // Example: periodStartDate = '2026-03-26' (UTC day start)
  const periodEnd = new Date(periodStartDate);
  periodEnd.setUTCDate(periodEnd.getUTCDate() + 1);

  // Get all users with events overlapping the period
  const { rows } = await db.query(
    `SELECT user_id, room_id, event_type, event_ts
     FROM participant_events
     WHERE event_ts >= $1::timestamptz - INTERVAL '1 day' -- safety margin
       AND event_ts < $2::timestamptz + INTERVAL '1 day'
     ORDER BY user_id, event_ts`,
    [periodStartDate, periodEnd.toISOString()]
  );

  // Pair joins/leaves per user -> build sessions
  const sessionsByUser = {};
  for (const r of rows) {
    const u = r.user_id;
    sessionsByUser[u] = sessionsByUser[u] || { stack: [], sessions: [] };
    const entry = sessionsByUser[u];

    if (r.event_type === 'join') {
      entry.stack.push(new Date(r.event_ts));
    } else if (r.event_type === 'leave') {
      // match last unmatched join
      const joinTs = entry.stack.pop() || null;
      if (joinTs) {
        entry.sessions.push({ start: joinTs, end: new Date(r.event_ts) });
      } else {
        // leave without join: ignore or create short session based on policy
      }
    }
  }

  // Close any open sessions at period end (policy dependent)
  for (const [user_id, data] of Object.entries(sessionsByUser)) {
    for (const openJoin of data.stack) {
      data.sessions.push({ start: openJoin, end: periodEnd }); // close at period end
    }

    // Calculate minutes overlapping the exact periodStartDate..periodEnd
    let totalSeconds = 0;
    const start = new Date(periodStartDate);
    const end = periodEnd;
    for (const s of data.sessions) {
      const overlapStart = s.start > start ? s.start : start;
      const overlapEnd = s.end < end ? s.end : end;
      if (overlapEnd > overlapStart) totalSeconds += (overlapEnd - overlapStart) / 1000;
    }
    const minutes = Math.ceil(totalSeconds / 60); // rounding choice

    // Upsert into reconciled_minutes
    await db.query(
      `INSERT INTO reconciled_minutes (user_id, period_start, period_type, minutes, source)
       VALUES ($1,$2,'day',$3,'app')
       ON CONFLICT (user_id, period_start, period_type, source)
       DO UPDATE SET minutes = EXCLUDED.minutes, reconciled_at = now()`,
      [user_id, periodStartDate, minutes]
    );
  }
}
Notes: • Decide rounding: ceil, floor, or nearest. Billing uses connection minutes; choose alignment that simplifies reconciliation. • For weekly buckets, choose week-start (e.g., Monday) and aggregate daily session overlaps into the week. 4) Reconciliation with Metrics Export API • Pull the metrics export (paginated) for the same period and aggregate per user, then compare with your app totals. Pseudo-steps: 1. Call Metrics Export API with time range for the day/week and request participant-level metrics. 2. Iterate pages, aggregate 'connected_seconds' (or similar metric) per participant user_id or participant SID. 3. Convert seconds -> minutes using same rounding rule. 4. Upsert into reconciled_minutes with source = 'metrics_export'. 5. Produce a reconciliation report: compare rows where source='app' vs source='metrics_export' to find deltas. Node.js sketch for metrics ingestion:
Copy code
async function ingestMetricsExport(startIso, endIso) {
  let nextPageToken = null;
  const perUserSeconds = new Map();

  do {
    const url = new URL('https://api.livekit.example/metrics/export'); // replace with actual endpoint
    url.searchParams.set('start', startIso);
    url.searchParams.set('end', endIso);
    if (nextPageToken) url.searchParams.set('pageToken', nextPageToken);

    const res = await fetch(url.toString(), {
      headers: { Authorization: `Bearer ${process.env.METRICS_API_KEY}` }
    });
    const payload = await res.json();
    // payload.items[]: each item should include participant id/user id and connected_seconds
    for (const item of payload.items || []) {
      const userId = item.user_id || item.participant_user_id || item.participant_sid;
      const seconds = item.connected_seconds || 0;
      perUserSeconds.set(userId, (perUserSeconds.get(userId) || 0) + seconds);
    }
    nextPageToken = payload.nextPageToken;
  } while (nextPageToken);

  // Upsert per user minutes
  for (const [userId, seconds] of perUserSeconds.entries()) {
    const minutes = Math.ceil(seconds / 60);
    await db.query(
      `INSERT INTO reconciled_minutes (user_id, period_start, period_type, minutes, source)
       VALUES ($1,$2,'day',$3,'metrics_export')
       ON CONFLICT (user_id, period_start, period_type, source)
       DO UPDATE SET minutes = EXCLUDED.minutes, reconciled_at = now()`,
      [userId, startIso.slice(0,10), minutes]
    );
  }
}
Important: replace the example Metrics Export URL and response field names with the actual endpoint and fields returned by your account’s metrics export. 5) Produce reconciliation report and take action • SQL to compare app vs metrics:
Copy code
SELECT a.user_id,
  a.minutes as app_minutes,
  m.minutes as metrics_minutes,
  (a.minutes - m.minutes) AS delta
FROM reconciled_minutes a
LEFT JOIN reconciled_minutes m
  ON a.user_id = m.user_id
  AND a.period_start = m.period_start
  AND a.period_type = m.period_type
  AND m.source = 'metrics_export'
WHERE a.source = 'app' AND a.period_start = $1;
• Use deltas to: ◦ Investigate missing events or timing mismatches. ◦ Adjust rounding rules. ◦ Flag users for manual review if deltas exceed a threshold. 6) Enforcement using reconciled totals • When a user attempts to join, check their, with interactive elements
n
this is all i needed my man and you didnt provide it: Important: replace the example Metrics Export URL and response field names with the actual endpoint and fields returned by your account’s metrics export.
t
I don't have the answer you're looking for. You could also try asking your question: • in one of the other Slack channels or • to https://deepwiki.com/livekit/livekit_composite which is trained on all LiveKit source code If you find the answer, please post it here to help others!