Sizing Guideline
This guide provides recommendations for sizing an XMPro deployment on Azure Kubernetes Service (AKS): how large to make the cluster's node pools and how to size the databases. The AWS Kubernetes (EKS) Sizing Guideline follows the same structure; only the cloud-specific offerings differ.
Overview
Small, medium, and large sizing estimates are provided. The small option starts with the minimum recommended resources and, generally, each subsequent size doubles the number of CPU cores and available RAM. Not all components experience the same increase in load, so the estimates may not increase at the same rate for all components.
Many factors influence the number of Apps and Data Streams a deployment can effectively run. These factors include:
- the number of data streams,
- how frequently the streams process data,
- the size of the data payload,
- the number of recommendations to be monitored,
- the number of apps and event boards being served,
- the complexity of apps and event boards (the number of elements and integration points),
- and the number of concurrent users accessing the apps and event boards.
As a rough guide, an example workload for a Medium-sized deployment would be:
- ~200 Data Streams running across
- ~15 Stream Hosts,
- serving data and triggering recommendations for ~10 Apps
Node Pool Sizing
On Kubernetes, XMPro sizing means two things: making sure the cluster's node pools have enough capacity for the product containers, and sizing the databases (covered in Database Sizing below). The example AKS Terraform module ships with evaluation-grade defaults (a single small system node, a single small application node, and a small shared database), sized for demos, trials, and development, not production load. The variables below are how you scale up from there.
Two Kubernetes terms are used throughout this section:
- A request is the amount of CPU or memory the Kubernetes scheduler reserves for a container. A node must have at least this much unreserved capacity free before a pod can be placed on it.
- A limit is the ceiling a container may consume. A container that exceeds its memory limit is terminated (OOM-killed); the XMPro chart deliberately sets memory limits but no CPU limits, so .NET startup is not CPU-throttled.
Requests drive scheduling (can the pod fit?); limits drive worst-case sizing (how much can everything consume at once?). The node pools need capacity for both.
Note
Node capacity is smaller than the advertised size. A node advertised as 8 GiB does not offer a full 8 GiB to workloads. AKS reserves memory and CPU for the kubelet and system processes, and what remains is the node's allocatable capacity. Budget roughly 85-90% of the advertised figure as usable.
Node pools
An AKS deployment runs two node pools: a system pool that hosts cluster-critical add-ons (CoreDNS, metrics-server, the AKS management components) and the platform controllers (Envoy Gateway, cert-manager, External Secrets Operator, Istio), and an application pool that hosts the XMPro product pods. Sizing the application pool means giving its nodes enough allocatable capacity for the sum of the product container requests, with headroom for the products to grow toward their limits under load.
| Node pool | Purpose | Default VM SKU | Default count |
|---|---|---|---|
system |
Add-ons, cluster controllers | Standard_B2s_v2 (2 vCPU, 8 GiB) |
1 |
app |
XMPro product pods | Standard_B2s_v2 (2 vCPU, 8 GiB) |
1 |
Note
B- and D-class VM SKUs are for test, development, or evaluation only. The Standard_B2s_v2 default, or a Standard_D-series SKU, works for a test or evaluation cluster at lower cost. B-series banks CPU credits when idle and throttles when the credits run out, so do not run a production site on it.
Per-product resource requests and limits
The table below lists the CPU and memory each XMPro product container reserves (request) and its memory ceiling (limit), taken from the Helm chart defaults. There are no CPU limits by design. These are the same chart resources regardless of whether you run on AKS or EKS. Subscription Manager (SM), Identity Server (IS), App Designer (AD), and Data Stream Designer (DS) are always deployed. On AKS the default in-cluster Stream Host (SH) is on (enable_default_stream_host = true); further Stream Hosts are deployed per Collection through the stream-hosts layer, which uses the streamhost chart. Its defaults are higher than the default Stream Host's and include a 500m CPU limit. The AI product is optional.
| Product | CPU request | Memory request | Memory limit |
|---|---|---|---|
| Subscription Manager (SM) | 100m | 512Mi | 1Gi |
| Identity Server (IS) | 100m | 1Gi | 2Gi |
| App Designer (AD) | 150m | 1Gi | 4Gi |
| Data Stream Designer (DS) | 150m | 2Gi | 4Gi |
| Stream Host (SH), default | 25m | 256Mi | 512Mi |
| Stream Host (SH), layer 4 | 100m | 256Mi | 1Gi |
| AI | 25m | 384Mi | 768Mi |
The standard product set (SM, IS, AD, DS) requests 500m CPU and 4.5Gi of memory, with a combined memory limit of 11Gi. The default Stream Host adds 25m / 256Mi (limit 512Mi), and each layer 4 Stream Host adds 100m / 256Mi (limits 500m / 1Gi). AI adds 25m / 384Mi (limit 768Mi).
Note
Add the Istio sidecar to every pod. enable_istio defaults to true, so every product pod and every Job pod gets an istio-proxy sidecar that requests a further 25m CPU and 128Mi (the cluster layer sets the proxy CPU request to 25m; memory is the Istio default). Istio runs the proxy as a native sidecar, so Kubernetes adds its request to the pod's own rather than absorbing it. For the standard product set that is another 100m / 512Mi, taking the steady-state request to 600m CPU and 5Gi; the default Stream Host adds 50m / 384Mi with its sidecar, each layer 4 Stream Host 125m / 384Mi, and AI 50m / 512Mi. Setting enable_istio = false in the application layer removes this overhead.
Note
Jobs run during install. Database-migration and licensing steps (ad-dbmigrate, ds-dbmigrate, sm-dbmigrate, ai-dbmigrate, licenses) run as short-lived Helm Jobs during install, each requesting 100m CPU / 256Mi (plus a sidecar) and capped at 200m CPU / 512Mi. They finish before the products reach steady state, so they do not count toward the always-on total above, but the application pool must have room to schedule them alongside the products during the initial deploy.
Translating requests into node size and count
To choose app_node_vm_size and app_node_count:
- Start from the request total above (~600m CPU, ~5Gi memory for the standard product set with sidecars; add each additional Stream Host and AI as enabled).
- Look up the vCPU and RAM of a candidate VM SKU in the Azure VM sizes documentation, then subtract the AKS system reservation to get its allocatable capacity.
- Size for the install peak, not the steady state. During an install the database-migration and licensing Jobs run concurrently with the products, each with its own sidecar. The Helm release is atomic: if one pod cannot be scheduled, the whole install rolls back after the 30-minute timeout, leaving nothing to inspect. Make sure the pool can hold the products and the Jobs at the same time.
- Because the product memory limits (~11Gi combined) are far higher than the memory requests, memory pressure under real load, not the request total, is what sizes a production pool. Size the application nodes toward the limit envelope, and confirm the true figure by measuring actual pod memory usage under representative load rather than trusting a static estimate.
- Set
app_node_countso the pool's combined allocatable capacity comfortably exceeds the workload, with headroom for node maintenance and pod rescheduling. Memory runs out before CPU does; if you are deciding what to grow, grow memory.
The defaults are a single application node and a single system node, which is why they are evaluation-grade: neither pool has redundancy. For production, run at least two system nodes (so the add-ons and controllers survive a node loss) and size the application pool to your measured load, adding capacity for each additional site on the cluster.
Node pool knobs
All node pool sizing lives in the AKS module's cluster layer (infra-k8s, deployed once per cluster). The table introduces each variable and its default.
| Variable | Default | Controls |
|---|---|---|
system_node_vm_size |
Standard_B2s_v2 |
VM SKU for the system pool (B/D-class for test/dev/evaluation only) |
system_node_count |
1 |
System pool node count (2+ recommended for add-on redundancy) |
app_node_vm_size |
Standard_B2s_v2 |
VM SKU for the application pool (B/D-class for test/dev/evaluation only) |
app_node_count |
1 |
Application pool node count (fixed size when autoscaling is off) |
kubernetes_version |
1.34 |
Kubernetes version of the cluster |
aks_sku_tier |
Free |
AKS control-plane tier (Premium required for LTS K8s versions) |
The root cluster module (_infra-k8s) also exposes app_autoscaling_enabled (default false) with app_min_count / app_max_count bounds for the AKS cluster autoscaler; the examples/layered/infra-k8s example does not pass these through, so add them to your own copy if you want the pool to autoscale.
To resize an application pool that already exists, change app_node_count and re-apply the cluster layer, or scale it directly:
az aks nodepool scale --resource-group <cluster-rg> --cluster-name <cluster> --name app --node-count 2
AI and RAG node pool
When the AI product's RAG feature is enabled, its backing services (Milvus vector store, Neo4j graph database, TimescaleDB, and the Mosquitto MQTT broker) run on a dedicated node pool provisioned by the optional AI infrastructure layer (infra-ai), applied once per cluster and shared by all sites. These services are memory-heavy, which is why they get their own pool rather than sharing the application nodes.
| Variable | Default | Controls |
|---|---|---|
ai_node_vm_size |
Standard_E4as_v5 |
VM SKU for the dedicated AI pool |
ai_node_count |
1 |
AI pool node count |
AI persistent volumes use the managed-csi-premium storage class by default (storage_class_name). Size this pool against the data volume you intend to embed and index; the single-node default is an evaluation starting point, not a production figure.
Scaling out
Beyond making nodes bigger, XMPro can scale horizontally by running more replicas of a product, but this requires shared-state coordination. The application layer (app) exposes enable_autoscale (default false), which turns on the Redis backplane (SignalR and distributed cache) so multiple replicas can coordinate; Redis can come from the site infrastructure layer (create_redis_cache = true provisions Azure Cache for Redis and outputs the connection string) or from an existing Redis you supply through redis_connection_string. Per-product replica counts (ad_replica_count, ds_replica_count, sm_replica_count, is_replica_count, ai_replica_count) default to 1, and enforce_single_replica (default false) hard-blocks any product from exceeding one replica when set. Products running more than one replica also need the *_persistence blocks enabled on an RWX-capable storage class (for example the AKS built-in azurefile-csi) so they share their Data Protection key ring.
At the cluster level, the root module's app_autoscaling_enabled turns on the AKS cluster autoscaler to add and remove application nodes automatically within the app_min_count / app_max_count bounds (see Node pool knobs). Per-product Horizontal Pod Autoscalers (HPAs) are still a chart follow-up, so horizontal scaling today means setting replica counts explicitly rather than relying on CPU-driven autoscaling of the pods themselves.
Production notes
- B- and D-class VM SKUs are for test, development, or evaluation only. For production use a memory-optimized
Standard_E-series SKU sized from measured load. - One node per pool is no redundancy. Raise
system_node_countto at least 2 and sizeapp_node_count(or the autoscaler bounds) so a node loss does not take the site down. Spread the pools across availability zones in regions that support them. aks_sku_tier = "Free"has no uptime SLA. UseStandardfor production, orPremiumif you need a long-term support Kubernetes version.
Database Sizing
XMPro uses Azure SQL Database. Each site gets its own SM, AD, and DS databases (plus an AI database when the AI product is enabled), on a SQL logical server created by the site infrastructure layer.
| Variable | Default | Notes |
|---|---|---|
db_use_elastic_pool |
true |
Place the SM, AD and DS databases in one shared Standard elastic pool |
db_elastic_pool_edtu |
50 |
Capacity of that pool (eDTU) |
db_sku_name |
S0 |
Per-database tier when db_use_elastic_pool = false |
db_max_size_gb |
2 |
Storage cap per database |
The following per-database sizing estimates use the Standard service tier and the DTU-based purchasing model (a blended measure of compute, storage, and IO resources).
| Deployment size | Azure SQL Database (each of SM, AD, DS) 1 |
|---|---|
| Small | Standard – 20 DTUs |
| Medium | Standard – 50 DTUs |
| Large | Standard – 100 DTUs |
Note
1 High volumes of recommendations may require additional compute and storage.
For production you have two levers, both in the site infrastructure layer (infra): keep the elastic pool and raise db_elastic_pool_edtu so the shared budget matches your workload, or set db_use_elastic_pool = false and give each database its own tier through db_sku_name when you want a guaranteed, isolated allocation rather than a shared burst budget. Raise db_max_size_gb for production data volumes either way.
MAGS Sizing
MAGS (Multi-Agent Generative Systems) is sized separately, across its database, application and GPU knowledge tiers. The node pool figures on this page do not include it.
Additional Resources
- Deploy XMPro on Azure Kubernetes (AKS) - the deployment guide
- Module README in the deployment repository - full variable reference
- Azure VM sizes
- Azure SQL Database DTU-based purchasing model
Last modified: September 28, 2026