After deploying the RealTheory collector to a Kubernetes cluster (See Installing RealTheory in a Cluster), the cluster and its associated objects, including any Recommendations, become available in the RealTheory API within minutes.
Overview
RealTheory analyzes how your Kubernetes workloads actually use CPU and memory, and recommends new requests and limits for each container. These right-size recommendations can be retrieved from a CI/CD pipeline, decoded, and applied to your deployment manifests so that the next deploy picks them up. This is a four-step process:
-
Retrieve the right-size recommendations for a workload using the
Recommendationsendpoint with a query scoped to the workload's cluster, namespace, and name.Example query:
classification=RightSizeWorkload AND metadata.clusterName=prod-east AND metadata.namespaceName=checkout AND metadata.objectName=checkout-api -
Choose the recommendation to use, skipping any recommendation that your team has dismissed or postponed in the RealTheory console.
-
Decode the container entries held in the recommendation's
properties. -
Apply the recommended values to your manifest, leaving any field without a recommendation unchanged.
A worked example and a sample Python implementation follow the steps.
Notes:
- All calls to RealTheory APIs must be authenticated and authorized. See Authentication for more details.
- Queries should be URL encoded; otherwise the request might be rejected.
Step 1: Retrieve the right-size recommendations for a workload
To get the right-size recommendations for a workload, call the method below on the K8sRecommendations endpoint.
GET <endpoint>/theory/api/v1/k8srecommendations/recommendations/search/query/{query}?index={index}&count={count}&order={order}&direction={direction}
where
{endpoint}- is the URL of the RealTheory SaaS portal for your account.{query}- is a query used to limit the results to right-size recommendations for a given workload.{index}- is the starting index of the recommendations to include in the list. This parameter is optional and defaults to 0 if not specified.{count}- is the maximum number of recommendations to return, up to a limit of 500. This parameter is optional and defaults to 100 if not specified.{order}- is a string containing the name of the attribute to sort on. This parameter is optional.{direction}- is a number that controls the direction of the sort. A value equal to or greater than 0 indicates ascending order; a value less than 0 indicates descending order. This parameter is optional and defaults to 0 if not specified.
The {query} should include the following conditions:
classification = RightSizeWorkload- returns only workload right-size recommendations, rather than other kinds of recommendation for the same workload.metadata.clusterName,metadata.namespaceName, andmetadata.objectName- scope the results to a single workload. Workload names are not unique across clusters and namespaces, so all three should be included.metadata.objectNameis the workload'smetadata.namein your manifest.metadata.objectType- is optional, and is the workload kind, for exampleDeployment,StatefulSet, orDaemonSet. Include it if a namespace can contain workloads of different kinds with the same name.
See Metadata for more details on the metadata fields available to queries.
Example
GET theory/api/v1/k8srecommendations/recommendations/search/query/classification=RightSizeWorkload AND metadata.clusterName=prod-east AND metadata.namespaceName=checkout AND metadata.objectName=checkout-api?index=0&count=100
which returns the right-size recommendations for the workload checkout-api in namespace checkout on cluster prod-east.
Notes:
- The query forms part of the path, so it must be URL encoded. For example, spaces become
%20. - See Queries for the full query syntax, including the supported operators and logical connectors.
- Use names exactly as they appear in Kubernetes: the cluster name as shown in the RealTheory console, and the namespace and workload names from your manifest.
- If your pipeline already knows the cluster, namespace, or workload ID, use
metadata.clusterId,metadata.namespaceId, ormetadata.objectIdin place ofmetadata.clusterName,metadata.namespaceName, ormetadata.objectNamerespectively. Queries on IDs are indexed and faster than queries on names, which are not indexed, so IDs should be preferred where available. If you know the workload ID,classification=RightSizeWorkload AND metadata.objectId=<workload-id>is sufficient on its own: the workload ID is guaranteed to be unique across your fleet, so the cluster and namespace conditions are not needed. See Objects for more details on querying related objects. - Results are paged, and the response's
x-total-countheader gives the total number of matches. A fully scoped query normally returns at most one recommendation. See Paging for more details on how to page through all results. - Expired recommendations are not returned.
- Your credentials only return recommendations for clusters you are authorized to view.
Output
Returns an array of Recommendation objects. See the worked example below for a sample right-size recommendation.
Step 2: Choose the recommendation to use
The query does most of the filtering. Three checks remain on your side:
- Skip dismissed recommendations, where
stateisDismissed. Your team has dismissed them in the RealTheory console. - Skip postponed recommendations, where
stateisPostponedandpostponedUntilis still in the future. Your team has postponed them in the RealTheory console, so they should not be applied yet. - If more than one recommendation remains, use the one with the latest
updatedtimestamp.
Notes:
stateis one ofNew,Read,Postponed,Dismissed, orUnknown. RespectingDismissedandPostponedmeans your pipeline honors the decisions your team makes in the RealTheory console. RealTheory's own automation applies the same rule.- You may also want to gate on how much evidence the recommendation rests on. See
right-size-metrics-spanandconfidenceunder Recommendation Properties below.
Step 3: Decode the container entries
The recommended values are carried in the recommendation's properties object, which is a map of string keys to string values.
Recommendation Properties
right-size-container-count: Represents the number of container entries, numbered from0, for example2. Its presence also identifies a right-size recommendation.right-size-container-{n}: Represents one entry per container, fornfrom0to count − 1. See Container Entries below.right-size-strategy: Represents the optimization strategy used. Supported values areperformance,cost, andbalanced.right-size-basis: Represents why the change is recommended. Supported values arecost,reliability, andperformance. The value may be a comma-separated list, for exampleperformance,cost, and should be treated as case-insensitive with whitespace trimmed. This property is optional.right-size-metrics-span: Represents how much metric history the recommendation is based on, for the least-observed container, for example3d2h30m. The value is expressed in days, hours, and minutes in descending order, and is capped at8d. This property is optional: if it is missing, the span is unknown, which is not the same as0m(measured, under a minute).
Note: The top-level confidence field of the Recommendation (None, Low, Medium, or High) is also useful for gating.
Container Entries
Each right-size-container-{n} value is a list of key=value fields separated by semicolons:
name=checkout-api;cpu-request=100m->250m;cpu-limit=<not set>->500m;memory-request=70Mi->128Mi;memory-limit=210Mi->256Mi;replicas=3;cost-impact=$40.32;
where
name- is the container name, matchingspec.containers[].namein your pod template, for examplecheckout-api.cpu-request- is the CPU request, in the form currently deployed value->recommended value, for example100m->250m.cpu-limit- is the CPU limit, in the same form, for example<not set>->500m.memory-request- is the memory request, in the same form, for example70Mi->128Mi.memory-limit- is the memory limit, in the same form, for example210Mi->256Mi.replicas- is the replica count used to estimate cost, for example3. This value is informational only: the recommendation never changes your replica count.cost-impact- is the estimated cost impact in US dollars, formatted as currency, for example$40.32. It can include thousands separators ($1,234.56) and can be negative. This value is informational only.
Notes:
- The left-hand side of
->is the value RealTheory observed deployed when it made the recommendation. The right-hand side is the recommended value. <not set>means there is no value: on the left, the field was not set on the deployed container; on the right, there is no recommendation for that field.
CPU and Memory Limits
RealTheory can be configured to include or leave out CPU limits and memory limits in its recommendations. Requests are always included. By default:
- CPU limits are not included, so the recommended value is
<not set>. - Memory limits are included, so the recommended value is a quantity, for example
512Mi.
With the default configuration, each container's cpu-limit ends in -><not set>. Following the rules in Step 4, your pipeline leaves the CPU limits in your manifests as they are and updates the memory limits. If you want CPU limits recommended as well, or do not want memory limits recommended, ask your RealTheory administrator to change the configuration. Your parser does not need to change: it simply sees quantities instead of <not set>, or the other way round.
Values are standard Kubernetes resource quantities that can be written straight into a manifest:
- CPU - millicores below one core (
2m,250m), otherwise cores with up to four decimal places (1,1.5,2.25). - Memory -
Gi,Mi, orKi, whichever represents the value exactly (1Gi,1.5Gi,128Mi,1500Ki).
Parsing Rules
Following these rules keeps your parser compatible with future additions to the format:
- Read
right-size-container-countas an integer. If it is missing or not a number, the recommendation is not a right-size recommendation. - For each
nfrom0to count − 1, readright-size-container-{n}. Skip any index that is missing or empty. - Split the entry on
;and ignore empty parts. The entry ends with a trailing;. - Split each part at the first
=into key and value, and trim whitespace from both. Ignore parts without an=. - Keys are lower-case. Ignore keys you do not recognize, as new fields may be added.
- Split each resource value at the first
->into deployed and recommended. If there is no->, treat the whole value as the recommended value. - Compare
<not set>case-insensitively, and treat it as "no value".
Note: Container names cannot contain ; or = (Kubernetes restricts them to lower-case letters, digits, and -), so no escaping is needed.
Step 4: Apply the values to your manifest
Each container entry maps to the container with the same name in your workload's pod template. For a Deployment, StatefulSet, or DaemonSet, that is spec.template.spec.containers[]:
cpu-request- maps toresources.requests.cpu.cpu-limit- maps toresources.limits.cpu.memory-request- maps toresources.requests.memory.memory-limit- maps toresources.limits.memory.
Apply these rules:
- If the recommended value is a quantity, set the manifest field to it.
- If the recommended value is
<not set>, leave the manifest field as it is. Do not add it and do not remove it: the recommendation has nothing to say about that field. RealTheory's own automation applies the same rule. - If a container is in the recommendation but not in your manifest, ignore it.
- If a container is in your manifest but not in the recommendation, leave it unchanged.
- Keep each request at or below its limit. Kubernetes rejects a container whose request exceeds its limit. This can happen when you apply a recommended request but keep an existing limit because the recommended limit is
<not set>. In that case, skip that request change and flag it for review rather than writing a manifest that will not deploy.
Detecting Manifest Drift
The deployed (left-hand) values tell you what the recommendation was calculated against. Before applying a field, compare its deployed value with the value currently in your manifest. If they differ, the manifest has changed since the recommendation was made: someone may have tuned it by hand, or it has not been deployed yet. Consider skipping those fields and flagging them for review.
Note: Compare the values as quantities, not as text: 1000m and 1 are the same CPU, and 1024Mi and 1Gi are the same memory.
Worked Example
The query from Step 1 returns the Recommendation below. It uses the default limit configuration, so both CPU limits are <not set> on the recommended side.
[
{
"id": "6f1c2a9e-3d4b-4c1a-9b8e-2f7a5d0c1e34",
"metadata": {
"objectType": "Deployment",
"objectName": "checkout-api",
"clusterName": "prod-east",
"namespaceName": "checkout",
...
},
"state": "New",
"classification": "RightSizeWorkload",
"confidence": "High",
"properties": {
"right-size-strategy": "balanced",
"right-size-basis": "cost",
"right-size-metrics-span": "8d",
"right-size-container-count": "2",
"right-size-container-0": "name=checkout-api;cpu-request=500m->250m;cpu-limit=<not set>-><not set>;memory-request=512Mi->384Mi;memory-limit=1Gi->512Mi;replicas=3;cost-impact=$40.32;",
"right-size-container-1": "name=log-shipper;cpu-request=100m->20m;cpu-limit=200m-><not set>;memory-request=64Mi->32Mi;memory-limit=128Mi->64Mi;replicas=3;cost-impact=$6.10;"
},
"updated": "2026-10-04T08:15:00Z",
"postponedUntil": null,
...
}
]
Note: Content has been omitted from the JSON response above for brevity.
The recommendation decodes to:
| Container | Field | Deployed | Recommended | Action |
|---|---|---|---|---|
checkout-api |
CPU request | 500m |
250m |
Set to 250m |
checkout-api |
CPU limit | not set | not set | Leave as is |
checkout-api |
Memory request | 512Mi |
384Mi |
Set to 384Mi |
checkout-api |
Memory limit | 1Gi |
512Mi |
Set to 512Mi |
log-shipper |
CPU request | 100m |
20m |
Set to 20m |
log-shipper |
CPU limit | 200m |
not set | Leave as is (200m) |
log-shipper |
Memory request | 64Mi |
32Mi |
Set to 32Mi |
log-shipper |
Memory limit | 128Mi |
64Mi |
Set to 64Mi |
Applied to the manifest:
spec:
template:
spec:
containers:
- name: checkout-api
resources:
requests:
cpu: 250m # was 500m
memory: 384Mi # was 512Mi
limits:
memory: 512Mi # was 1Gi
- name: log-shipper
resources:
requests:
cpu: 20m # was 100m
memory: 32Mi # was 64Mi
limits:
cpu: 200m # unchanged: no recommendation
memory: 64Mi # was 128Mi
Sample Implementation (Python)
This sample covers Steps 1 to 4. It requires Python 3.11 or later, uses requests, and works on a manifest already loaded into a dictionary, for example with PyYAML.
from datetime import datetime, timezone
from urllib.parse import quote
import requests
NOT_SET = "<not set>"
RESOURCE_FIELDS = {
"cpu-request": ("requests", "cpu"),
"cpu-limit": ("limits", "cpu"),
"memory-request": ("requests", "memory"),
"memory-limit": ("limits", "memory"),
}
def get_recommendations(base_url, auth_header, cluster, namespace, name, kind=None):
"""Return the right-size recommendations for one workload, across all pages."""
query = (
f"classification=RightSizeWorkload"
f" AND metadata.clusterName={cluster}"
f" AND metadata.namespaceName={namespace}"
f" AND metadata.objectName={name}"
)
if kind:
query += f" AND metadata.objectType={kind}"
url = f"{base_url}/theory/api/v1/k8srecommendations/recommendations/search/query/{quote(query, safe='')}"
index, count, results = 0, 100, []
while True:
response = requests.get(url, params={"index": index, "count": count}, headers={"Authorization": auth_header}, timeout=30)
response.raise_for_status()
page = response.json()
results.extend(page)
if len(page) < count:
return results
index += count
def is_postponed(recommendation):
until = recommendation.get("postponedUntil")
if recommendation.get("state") != "Postponed" or not until:
return False
return datetime.fromisoformat(until.replace("Z", "+00:00")) > datetime.now(timezone.utc)
def choose(recommendations):
"""Return the newest recommendation that isn't dismissed or postponed, or None."""
candidates = [r for r in recommendations if r.get("state") != "Dismissed" and not is_postponed(r)]
return max(candidates, key=lambda r: r.get("updated", ""), default=None)
def normalize(value):
value = value.strip()
return None if value == "" or value.lower() == NOT_SET else value
def split_transition(value):
"""Split 'deployed->recommended'; a value without '->' is the recommended value."""
if "->" in value:
deployed, recommended = value.split("->", 1)
return normalize(deployed), normalize(recommended)
return None, normalize(value)
def decode(recommendation):
"""Decode the container entries into dictionaries keyed by field name."""
properties = recommendation.get("properties") or {}
try:
count = int(properties.get("right-size-container-count", ""))
except ValueError:
return []
containers = []
for n in range(count):
entry = properties.get(f"right-size-container-{n}")
if not entry:
continue
fields = {}
for part in entry.split(";"):
if "=" in part:
key, value = part.split("=", 1)
fields[key.strip()] = value.strip()
container = {"name": fields.get("name")}
for key in RESOURCE_FIELDS:
if key in fields:
container[key] = split_transition(fields[key])
containers.append(container)
return containers
def apply(manifest, containers):
"""Write recommended values into the pod template; fields without a recommendation are left as they are."""
targets = {c["name"]: c for c in manifest["spec"]["template"]["spec"].get("containers", [])}
for container in containers:
target = targets.get(container["name"])
if target is None:
continue
resources = target.setdefault("resources", {})
for key, (section, resource) in RESOURCE_FIELDS.items():
_, recommended = container.get(key, (None, None))
if recommended is not None:
resources.setdefault(section, {})[resource] = recommended
Example
recommendations = get_recommendations("https://<endpoint>", AUTH_HEADER, cluster="prod-east", namespace="checkout", name="checkout-api", kind="Deployment")
chosen = choose(recommendations)
if chosen is not None:
apply(manifest, decode(chosen))
Notes:
AUTH_HEADERis the value of theAuthorizationheader for your RealTheory API credentials. See Authentication for more details.- The sample leaves out the request-versus-limit check and drift detection described in Step 4, because both require Kubernetes quantity parsing.