Capacity Licensing
This feature is available in the Enterprise Plan and above. For more information, see our pricing plans or contact our sales team.
This guide explains how to configure and monitor capacity-based licensing in self-hosted Upbound Spaces. Capacity licensing provides a simplified billing model for disconnected or air-gapped environments where automated usage reporting isn't possible.
Spaces v1.15 and later support Capacity Licensing as an
alternative to the traditional usage-based billing model described in the
Self-Hosted Space Billing guide.
Overview
Capacity licensing allows organizations to purchase a fixed capacity of resources upfront. The Spaces software tracks usage locally and provides visibility into consumption against your purchased capacity, all without requiring external connectivity to Upbound's services.
Key concepts
- Resource Hours: The primary billing unit representing all resources managed by Crossplane over time. This includes managed resources, composites (XRs), claims (XRCs), and all composed resources - essentially everything Crossplane manages. The system aggregates resource counts over each hour using trapezoidal integration to accurately account for changes in resource count throughout the hour.
- Operations: The number of Operations invoked by Crossplane.
- License Capacity: The total amount of resource hours and operations included in your license.
- Usage Tracking: Continuous monitoring of consumption with real-time utilization percentages.
How it works
- Upbound provides you with a license file containing your purchased capacity
- You configure a
SpaceLicensein your Spaces cluster - The metering system automatically:
- Collects measurements from all control planes every minute
- Aggregates usage data into hourly intervals
- Stores usage data in a local PostgreSQL database
- Updates the
SpaceLicensestatus with current consumption
Prerequisites
PostgreSQL database
Capacity licensing requires a PostgreSQL database to store usage measurements. You can use:
- An existing PostgreSQL instance
- A managed PostgreSQL service (AWS RDS, Azure Database, Google Cloud SQL)
- A PostgreSQL instance deployed in your cluster
The database must be:
- Accessible from the Spaces cluster
- Configured with a dedicated database and credentials
Example: Deploy PostgreSQL with CloudNativePG
If you don't have an existing PostgreSQL instance, you can deploy one in your cluster using CloudNativePG (CNPG). CNPG is a Kubernetes operator that manages PostgreSQL clusters.
- Install the CloudNativePG operator:
kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml
- Create a PostgreSQL cluster for metering:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: metering-postgres
namespace: upbound-system
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:16
bootstrap:
initdb:
database: metering
owner: metering
postInitApplicationSQL:
- ALTER ROLE "metering" CREATEROLE;
storage:
size: 5Gi
# Optional: Configure resources for production use
# resources:
# requests:
# memory: "512Mi"
# cpu: "500m"
# limits:
# memory: "1Gi"
# cpu: "1000m"
---
apiVersion: v1
kind: Secret
metadata:
name: metering-postgres-app
namespace: upbound-system
labels:
cnpg.io/reload: "true"
stringData:
username: metering
password: "your-secure-password-here"
type: kubernetes.io/basic-auth
kubectl apply -f metering-postgres.yaml
- Wait for the cluster to be ready:
kubectl wait --for=condition=ready cluster/metering-postgres -n upbound-system --timeout=5m
- You can access the PostgreSQL cluster at
metering-postgres-rw.upbound-system.svc.cluster.local:5432.
For production deployments, consider:
- Increasing
instancesto 3 for high availability - Configuring backups to object storage
- Setting appropriate resource requests and limits
- Using a dedicated storage class with good I/O performance
License file
Contact your Upbound sales representative to obtain a license file for your organization. The license file contains:
- Your unique license ID
- Purchased capacity (resource hours and operations)
- License validity period
- Any usage restrictions (such as cluster UUID pinning)
Configuration
Step 1: Create database credentials secret
Create a Kubernetes secret containing your PostgreSQL password using the pgpass format:
# Create a pgpass file with format: hostname:port:database:username:password
# Note: The database name and username must be 'metering'
# For CNPG clusters, use the read-write service endpoint: <cluster-name>-rw.<namespace>.svc.cluster.local
echo "metering-postgres-rw.upbound-system.svc.cluster.local:5432:metering:metering:your-secure-password-here" > pgpass
# Create the secret
kubectl create secret generic metering-postgres-credentials \
-n upbound-system \
--from-file=pgpass=pgpass
# Clean up the pgpass file
rm pgpass
The secret must contain a single key:
pgpass: PostgreSQL password file in the formathostname:port:metering:metering:password
The database name and username are fixed as metering. Ensure your PostgreSQL instance has a database named metering with a user metering that has appropriate permissions.
If you deployed PostgreSQL using CNPG as shown in the example above, the password should match what you set in the metering-postgres-app secret.
For production environments, consider using external secret management solutions:
- External Secrets Operator
- Cloud-specific secret managers (AWS Secrets Manager, Azure Key Vault, GCP Secret Manager)
Step 2: Enable metering in Spaces
Enable the metering feature when installing or upgrading Spaces:
- Helm
- up CLI
helm -n upbound-system upgrade --install spaces ... \
--set "metering.enabled=true" \
--set "metering.storage.postgres.connection.url=metering-postgres-rw.upbound-system.svc.cluster.local:5432" \
--set "metering.storage.postgres.connection.credentials.secret.name=metering-postgres-credentials" \
--set "metering.interval=1m" \
--set "metering.workerCount=10" \
--set "metering.aggregationInterval=1h" \
--set "metering.measurementRetentionDays=30"
...
up space init ... \
--set "metering.enabled=true" \
--set "metering.storage.postgres.connection.url=metering-postgres-rw.upbound-system.svc.cluster.local:5432" \
--set "metering.storage.postgres.connection.credentials.secret.name=metering-postgres-credentials" \
--set "metering.interval=1m" \
--set "metering.workerCount=10" \
--set "metering.aggregationInterval=1h" \
--set "metering.measurementRetentionDays=30"
...
Configuration options
| Option | Default | Description |
|---|---|---|
metering.enabled | false | Enable the metering feature |
metering.storage.postgres.connection.url | - | PostgreSQL host and port (format: host:port, required) |
metering.storage.postgres.connection.credentials.secret.name | - | Name of the secret containing PostgreSQL credentials (required) |
metering.storage.postgres.connection.sslmode | require | SSL mode for PostgreSQL connection (disable, allow, prefer, require, verify-ca, verify-full) |
metering.storage.postgres.connection.ca.name | - | Name of the secret containing CA certificate for TLS connections (optional) |
metering.interval | 1m | How often to collect measurements from control planes |
metering.workerCount | 10 | Number of parallel workers for measurement collection |
metering.aggregationInterval | 1h | How often to aggregate measurements into hourly usage data |
metering.measurementRetentionDays | 30 | Days to retain raw measurements (0 = indefinite) |
Database sizing and retention
The metering system uses two PostgreSQL tables to track usage:
Raw measurements table (measurements):
- Stores point-in-time snapshots collected every measurement interval (default: 1 minute)
- One row per control plane per interval
- Affected by the
measurementRetentionDayssetting - Used for detailed auditing and troubleshooting
Aggregated usage table (hourly_usage):
- Stores hourly aggregated resource hours and operations per license
- One row per hour per license
- Never deleted (required for accurate license tracking)
- Grows much slower than raw measurements
Storage sizing guidelines
Estimate your PostgreSQL storage needs based on these factors:
| Deployment Size | Control Planes | Measurement Interval | Retention Days | Raw Measurements | Indexes & Overhead | Total Storage |
|---|---|---|---|---|---|---|
| Small | 10 | 1m | 30 | ~85 MB | ~40 MB | ~125 MB |
| Medium | 50 | 1m | 30 | ~430 MB | ~215 MB | ~645 MB |
| Large | 200 | 1m | 30 | ~1.7 GB | ~850 MB | ~2.5 GB |
| Large (90-day retention) | 200 | 1m | 90 | ~5.2 GB | ~2.6 GB | ~7.8 GB |
The aggregated hourly usage table adds minimal overhead (~50 KB per year per license).
Formula for custom calculations:
Daily measurements per control plane = (24 * 60) / interval_minutes
Total rows = control_planes × daily_measurements × retention_days
Storage (MB) ≈ (total_rows × 200 bytes) / 1,048,576 × 1.5 (with indexes)
Retention behavior
The measurementRetentionDays setting controls retention of raw measurement data:
- Default: 30 days - Balances audit capabilities with storage efficiency
- Set to 0: Disables cleanup, retains all raw measurements indefinitely
- Cleanup runs: Every aggregation interval (default: hourly)
- What's kept forever: Aggregated hourly usage data (needed for license tracking)
- What's cleaned up: Raw point-in-time measurements older than retention period
Recommendations:
- 30 days: For most troubleshooting and short-term auditing
- 60 to 90 days: For environments requiring extended audit trails
- Unlimited (0): Only for environments with ample storage or specific compliance requirements
Increasing retention period linearly increases storage requirements for raw measurements. The aggregated hourly data is always retained regardless of this setting.
Step 3: Apply your license
Use the up CLI to apply your license file:
up space license apply /path/to/license.json
This command automatically:
- Creates a secret containing your license file in the
upbound-systemnamespace - Creates the
SpaceLicenseresource configured to use that secret
You can specify a different namespace for the license secret using the --namespace flag:
up space license apply /path/to/license.json --namespace my-namespace
Alternative: Manual kubectl approach
If you prefer not to use the up CLI, you can manually create the resources:
- Create the license secret:
kubectl create secret generic space-license \
-n upbound-system \
--from-file=license.json=/path/to/license.json
- Create the SpaceLicense resource:
apiVersion: admin.spaces.upbound.io/v1alpha1
kind: SpaceLicense
metadata:
name: space
spec:
secretRef:
name: space-license
namespace: upbound-system
key: license.json
kubectl apply -f spacelicense.yaml
You must name the SpaceLicense resource space. This resource is a singleton and only one can exist in the cluster.
Monitoring usage
Check license status
Use the up CLI to view your license details and current usage:
up space license show
Example output:
Spaces License Status: Valid (License is valid)
Created: 2024-01-01T00:00:00Z
Expires: 2025-01-01T00:00:00Z
Plan: enterprise
Resource Hour Limit: 1000000
Operation Limit: 500000
Enabled Features:
- spaces
- query-api
- backup-restore
The output shows:
- License validity status and any validation messages
- Creation and expiration dates
- Your commercial plan tier
- Capacity limits for resource hours and operations
- Enabled features in your license
- Any restrictions (such as cluster UUID pinning)
Alternative: View detailed status with kubectl
For detailed information including usage statistics, use kubectl:
kubectl get spacelicense space -o yaml
Example output showing usage data:
apiVersion: admin.spaces.upbound.io/v1alpha1
kind: SpaceLicense
metadata:
name: space
spec:
secretRef:
name: space-license
namespace: upbound-system
status:
conditions:
- type: LicenseValid
status: "True"
reason: Valid
message: "License is valid"
id: "lic_abc123xyz"
plan: "enterprise"
capacity:
resourceHours: 1000000
operations: 500000
usage:
resourceHours: 245680
operations: 12543
resourceHoursUtilization: "24.57%"
operationsUtilization: "2.51%"
firstMeasurement: "2024-01-15T10:00:00Z"
lastMeasurement: "2024-02-10T14:30:00Z"
createdAt: "2024-01-01T00:00:00Z"
expiresAt: "2025-01-01T00:00:00Z"
enabledFeatures:
- "spaces"
- "query-api"
- "backup-restore"
Understanding the status fields
| Field | Description |
|---|---|
status.id | Unique license identifier |
status.plan | Your commercial plan (community, standard, enterprise) |
status.capacity | Total capacity included in your license |
status.usage.resourceHours | Total resource hours consumed |
status.usage.operations | Total operations performed |
status.usage.resourceHoursUtilization | Percentage of resource hours capacity used |
status.usage.operationsUtilization | Percentage of operations capacity used |
status.usage.firstMeasurement | When usage tracking began |
status.usage.lastMeasurement | Most recent usage update |
status.expiresAt | License expiration date |
Monitor with kubectl
Watch your license utilization in real-time:
kubectl get spacelicense space -w
Short output format:
NAME PLAN VALID REASON AGE
space enterprise True Valid 45d
Managing licenses
Updating your license
To update your license with a new license file (for example, when renewing or upgrading capacity), apply the new license:
up space license apply /path/to/new-license.json
This command replaces the existing license secret and updates the SpaceLicense resource.
Removing a license
To remove a license:
up space license remove
This command:
- Prompts for confirmation before proceeding
- Removes the license secret
To skip the confirmation prompt, use the --force flag:
up space license remove --force
Troubleshooting
License not updating
If the license status doesn't update with usage data:
- Check metering controller logs:
kubectl logs -n upbound-system deployment/spaces-controller -c metering
2Check if the system captures your measurements:
# Connect to PostgreSQL and query the measurements table
kubectl exec -it <postgres-pod> -- psql -U <user> -d <database> \
-c "SELECT COUNT(*) FROM measurements WHERE timestamp > NOW() - INTERVAL '1 hour';"
High utilization warnings
If you're approaching your capacity limits:
- Review resource usage by control plane to identify high consumers
- Contact your Upbound sales representative to discuss capacity expansion
- Optimize managed resources by cleaning up unused resources
License validation failures
If your license shows as invalid:
- Check expiration date:
kubectl get spacelicense space -o jsonpath='{.status.expiresAt}' - Verify license file integrity: Ensure the secret contains valid JSON
- Check for cluster UUID restrictions: Upbound pins some licenses to specific clusters
- Review controller logs for detailed error messages
Differences from traditional billing
Capacity licensing
- ✅ Works in disconnected environments
- ✅ Provides real-time usage visibility
- ✅ No manual data export required
- ✅ Requires PostgreSQL database
- ✅ Fixed capacity model
Traditional billing (object storage)
- ❌ Requires periodic manual export
- ❌ Delayed visibility into usage
- ✅ Works with S3/Azure Blob/GCS
- ❌ Requires cloud storage access
- ✅ Pay-as-you-go model
Best practices
Database management
- Regular backups: Back up your metering database regularly to preserve usage history
- Monitor database size: Set appropriate retention periods to manage storage growth
- Use managed databases: Consider managed PostgreSQL services for production
- Connection pooling: Use connection pooling for better performance at scale
License management
- Monitor utilization: Set up alerts before reaching 80% capacity
- Plan renewals early: Start renewal discussions 60 days before expiration
- Track grace periods: Note the
gracePeriodEndsAtdate for planning - Secure license files: Treat license files as sensitive credentials
Operational monitoring
- Set up dashboards: Create Grafana dashboards for usage trends
- Enable alerting: Configure alerts for high utilization and expiration
- Regular audits: Periodically review usage patterns across control planes
- Capacity planning: Use historical data to predict future capacity needs
Next steps
- Learn about Observability to monitor your Spaces deployment
- Explore Backup and Restore to protect your control plane data
- Review Self-Hosted Space Billing for the traditional billing model
- Contact Upbound Sales to discuss capacity licensing options