Accounting

When running a shared cluster for multiple tenants, you may want to monitor cluster usage or even compute accountable usage numbers per tenant. There are several ways to achieve this goal, all with their own benefits and drawbacks.

Parsing raw events.xml files

You could use tenant overrides to enforce meetingKeepEvents=true during meeting creation, and then collect and analyze the events.xml files from all your BBB servers.

This is the most detailed, but also most invasive approach because events.xml contains WAY more data than necessary, including participant names and chat messages, even private chats. You should definitely speak to your Data Protection Officer to make sure this is okay.

You'd also need to parse and process events.xml yourself. I'm not aware of any ready-to-use tool to get usage statistics out of those even streams.

Collecting analytics data

Similar to the events.xml approach you can use tenant overrides to enforce meetingKeepEvents=true during meeting creation, but instead of fetching and parsing raw events.xml files, you enable ANALYTICS_STORE in BBBLB and collect analytics data on your BBBLB server. You will end up with one analytics file per meeting, neatly separated by tenant.

These analytics JSON files are pre-processed and way easier to parse than raw events.xml files. They no longer contain (private) chats, but you will still find participant names in there, so don't forget to speak to your Data Protection Officer about it. You also need to get rid of the events.xml files on your BBB servers in a timely manner to not get in conflict with GDPR.

Detailed metrics with POLL_STATS

Added in version 0.0.17.

Caution

This is an experimental feature.

BBBLB can collect detailed metrics and store them in the database, allowing you to run your own SQL queries to get any statistics you may need. Once activated with the POLL_STATS setting, the meeting poller will store the current users, voice and video counts for each running meeting on each server poll. Those metrics do not contain any personal data, which makes this approach very GDPR friendly.

The POLL_STATS feature is disabled by default, because the database table will grow by one row per meeting per POLL_INTERVAL and there is no automatic cleanup. This adds up quickly, especially for large or busy clusters. Make sure to delete old rows regularly to keep your database size in check.

The meeting_stats table is structured similar to a time series database. Each row has a timestamp (ts), the uuid of the meeting, the reusable external meeting_id that was used to create the meeting, the owning tenant (tenant_fk), and three metric values named users, voice and video.

The timestamp (ts) will be the exact same for all measurements taken during a single poll interval. It marks the start of the poll interval, not the exact time of an individual measurement. This is is done on purpose so you can group by the timestamp to get a consistend view of the entire cluster at a specific time.

Here is an example that calculates user counts for the entire cluster over time. It uses the fact mentioned above that all measurements taken during a single poll interval will have the exact same timestamp.

SELECT
  ts,
  COUNT(*) as meetings,
  SUM(users) AS users
FROM meeting_stats
GROUP BY ts
ORDER BY ts

Here is a more complex PostgreSQL example. It fetches all rows in a certain time range, calculates min/max/avg values per meeting (per uuid), then groups those together by tenant_fk to get meaningful aggregated values per tenant.

SELECT
  tenants.name AS tenant,
  /* Number of meetings */
  COUNT(*) AS meetings,
  /* Total number of meeting minutes spent by all users combined */
  SUM(users_avg * EXTRACT(epoch FROM duration)) / 60 AS meeting_minutes,
  /* Average meeting duration in minutes */
  AVG(EXTRACT(epoch FROM duration)) / 60 AS duration_avg,
  /* Average meeting size */
  AVG(users_avg) AS users_avg,
  /* Maximum meeting size */
  MAX(users_max) AS users_max,
  /* Number of meetings with more than 25 users peak */
  COUNT(CASE WHEN users_max > 100 THEN 1 END) AS large_25,
  /* Number of meetings with more than 100 users peak */
  COUNT(CASE WHEN users_max > 100 THEN 1 END) AS large_100
FROM (
    SELECT
      tenant_fk,
      uuid,
      MAX(ts) - MIN(ts) as duration,
      AVG(users) AS users_avg,
      MAX(users) AS users_max
    FROM meeting_stats
    WHERE ts::date <@ '[2026-02-01,2026-03-01)'::daterange
    GROUP BY tenant_fk, uuid
)
INNER JOIN tenants ON (tenant_fk = tenants.id)
GROUP BY tenants.name