Summary
Each Flagsmith Instance (Private Cloud or Self-Hosted) should report its own usage to the central Control Plane once an hour. The Control Plane already accepts these reports at POST /v1/usage; this issue is the sending half that
lives inside Flagsmith.
The report is a "usage snapshot": a point-in-time count of seats, projects, and API calls. Flagsmith staff use it to compare what a customer actually uses against what their licence entitles them to.
How it works
A recurring task on the task processor runs hourly. For each licensed organisation on this Instance, the run:
- Builds one usage snapshot from that organisation's own data.
- Reads the licence signature (
OrganisationLicence.signature).
- POSTs the snapshot to the Control Plane with the signature as a bearer token.
In this version we expect one licensed organisation per Instance, so most runs send a single snapshot. The loop keeps us correct if there is more than one.
What goes in a snapshot
Both deployment types send:
timestamp — the hour the snapshot covers (UTC). We send the most recent complete hour.
deployment_type — private_cloud or self_hosted (see open question).
seat_count — total users in the organisation, dormant included (organisation.num_seats).
instance_version — the Flagsmith version running, as plain semver (e.g. 2.140.1). No v prefix, no latest/unknown.
Self-Hosted adds:
project_count — non-deleted projects (organisation.projects.filter(deleted_at__isnull=True).count()).
Private Cloud adds:
api_call_total — total SDK API calls this period.
- A breakdown by type:
flags, identities, traits, environment_documents.
- A per-project breakdown:
project_id, project_name, api_call_count.
All API-call counts come from app_analytics — reuse analytics_db_service.get_usage_data(), which already supports per-type and per-project filtering (the same source as the GET /organisations/{id}/usage-data/ endpoint).
Sending it
- Endpoint:
POST /v1/usage on the Control Plane (base URL from config).
- Auth:
Authorization: Bearer <base64url(OrganisationLicence.signature)>.
- Expected responses:
202 — accepted (also returned for a duplicate hour — safe to ignore).
400 — payload rejected. Log it; the snapshot was malformed.
401 — auth failed (bad/expired/revoked licence). Log it.
429 — rate limited. Back off; do not retry aggressively.
5xx — server error. Retry on the next scheduled run, not in a tight loop.
Re-sending the same hour is harmless — the Control Plane drops duplicates by (customer, timestamp). A missed run that catches up next hour is fine.
Configuration
New settings, all optional so existing deployments stay silent until turned on:
CONTROL_PLANE_URL — base URL. If unset, the task is a no-op.
CONTROL_PLANE_DEPLOYMENT_TYPE — private_cloud or self_hosted. Needed because the codebase cannot currently tell the two apart (only SaaS / enterprise / OSS are detectable). See open question.
The licence signature is read from the database (OrganisationLicence), so it needs no new setting.
If the Control Plane URL is missing, skip the run quietly — never crash the task processor.
Open question
- Deployment type. There is no existing flag for Private Cloud vs Self-Hosted (both are non-SaaS; only
is_saas() / is_enterprise() / is_oss() exist). The simplest fix is a new CONTROL_PLANE_DEPLOYMENT_TYPE setting per Instance. This may become moot if the payload shape is unified — see Flagsmith/control-plane#15.
Acceptance criteria
Summary
Each Flagsmith Instance (Private Cloud or Self-Hosted) should report its own usage to the central Control Plane once an hour. The Control Plane already accepts these reports at
POST /v1/usage; this issue is the sending half thatlives inside Flagsmith.
The report is a "usage snapshot": a point-in-time count of seats, projects, and API calls. Flagsmith staff use it to compare what a customer actually uses against what their licence entitles them to.
How it works
A recurring task on the task processor runs hourly. For each licensed organisation on this Instance, the run:
OrganisationLicence.signature).In this version we expect one licensed organisation per Instance, so most runs send a single snapshot. The loop keeps us correct if there is more than one.
What goes in a snapshot
Both deployment types send:
timestamp— the hour the snapshot covers (UTC). We send the most recent complete hour.deployment_type—private_cloudorself_hosted(see open question).seat_count— total users in the organisation, dormant included (organisation.num_seats).instance_version— the Flagsmith version running, as plain semver (e.g.2.140.1). Novprefix, nolatest/unknown.Self-Hosted adds:
project_count— non-deleted projects (organisation.projects.filter(deleted_at__isnull=True).count()).Private Cloud adds:
api_call_total— total SDK API calls this period.flags,identities,traits,environment_documents.project_id,project_name,api_call_count.All API-call counts come from
app_analytics— reuseanalytics_db_service.get_usage_data(), which already supports per-type and per-project filtering (the same source as theGET /organisations/{id}/usage-data/endpoint).Sending it
POST /v1/usageon the Control Plane (base URL from config).Authorization: Bearer <base64url(OrganisationLicence.signature)>.202— accepted (also returned for a duplicate hour — safe to ignore).400— payload rejected. Log it; the snapshot was malformed.401— auth failed (bad/expired/revoked licence). Log it.429— rate limited. Back off; do not retry aggressively.5xx— server error. Retry on the next scheduled run, not in a tight loop.Re-sending the same hour is harmless — the Control Plane drops duplicates by
(customer, timestamp). A missed run that catches up next hour is fine.Configuration
New settings, all optional so existing deployments stay silent until turned on:
CONTROL_PLANE_URL— base URL. If unset, the task is a no-op.CONTROL_PLANE_DEPLOYMENT_TYPE—private_cloudorself_hosted. Needed because the codebase cannot currently tell the two apart (only SaaS / enterprise / OSS are detectable). See open question.The licence signature is read from the database (
OrganisationLicence), so it needs no new setting.If the Control Plane URL is missing, skip the run quietly — never crash the task processor.
Open question
is_saas()/is_enterprise()/is_oss()exist). The simplest fix is a newCONTROL_PLANE_DEPLOYMENT_TYPEsetting per Instance. This may become moot if the payload shape is unified — see Flagsmith/control-plane#15.Acceptance criteria
/v1/usage.app_analyticsfor API-call counts (no new counting logic).202/400/401/429/5xxwithout crashing and logs failures.instance_versionis sent as clean semver.