# Deprecations Source: https://docs.thoras.ai/deprecations Active deprecations in Thoras and their removal timelines. This page tracks deprecations in Thoras, including the announcement date, removal date, and migration guidance for each. See the [Support and Deprecation Policy](/policies/support-policy) for the process and timelines behind these entries. ## Active deprecations ### `featureFlags.enableSimpleAuthSecret` (Helm chart) | Announced | Removal | | - | - | | Chart 5.0.0 | Chart 6.0.0 | The toggle for component-to-component bearer authentication moved from `featureFlags.enableSimpleAuthSecret` to `apiClientSecret.enabled`. The legacy field continues to work as an alias in chart 5.x. Setting both to conflicting values fails the chart render. **Migration:** rename the key in `values.yaml`. ```yaml theme={null} # Before featureFlags: enableSimpleAuthSecret: true # After apiClientSecret: enabled: true ``` ### `featureFlags.enableLegacySecretSeeding` and legacy Secrets (Helm chart) | Announced | Removal | | - | - | | Chart 5.0.0 | Chart 6.0.0 | Chart 5.0 moved credential seeding out of Helm templates and into the in-cluster config-controller. The chart 4.x `api-client-secret` and `thoras-timescale-password` Secrets are still rendered when `featureFlags.enableLegacySecretSeeding` is `true` (the default) so the config-controller can migrate their values on upgrade. Nothing consumes them afterwards. The flag and both Secrets are removed in chart 6.0. **Migration:** set `featureFlags.enableLegacySecretSeeding: false` on fresh installs, and on existing installs once the first 5.x upgrade has completed and all pods are healthy. See [Legacy secret seeding](/installation/credentials#legacy-secret-seeding). # Compliance & Security Source: https://docs.thoras.ai/faq/compliance Learn about Thoras security certifications and data protection practices ## SOC 2 Type 2 Certified SOC 2 Certified Thoras is SOC 2 Type 2 certified, demonstrating our commitment to maintaining the highest standards of security, availability, and confidentiality. Our SOC 2 certification provides assurance that we have established appropriate controls and safeguards to protect your data and systems. ## Self-Hosted. Your Data Stays With You Thoras is a self-hosted solution, meaning all your data remains within your own cluster, never leaving your environment. This approach provides several key security benefits: ### Complete Data Control * All metrics, recommendations, and operational data stays within your Kubernetes clusters * No data transmission to external third-party services * Full ownership and control over your sensitive information ### Enhanced Security Posture * Nothing leaves your environment (unless you enable optional Cloud Sync) * Eliminates external data exposure risks * Maintains compliance with your organization's data governance policies * Reduces attack surface by keeping data within your secured perimeter ### Secure by Default * Internal API calls between Thoras components are authenticated with a shared bearer token. * The dashboard requires authentication out of the box, with single sign-on available through Okta, Azure Entra ID, or any OIDC-compliant identity provider. * Optional least-privilege network policies for every component ### Regulatory Compliance * Helps meet strict data residency requirements * Supports GDPR, HIPAA, FedRAMP, and other regulatory compliance frameworks * Enables [audit trails](/guides/audit-trail) within your controlled environment ## Questions About Security? For specific security questions or to request additional compliance documentation, email [support@thoras.ai](mailto:support@thoras.ai). # Integrations Source: https://docs.thoras.ai/faq/integrations How Thoras works alongside KEDA, Cluster Autoscaler, and Karpenter Thoras is designed to complement your existing Kubernetes autoscaling infrastructure, not replace it. This guide explains how Thoras integrates with KEDA, Cluster Autoscaler, and Karpenter. For HPA-specific setup and configuration, see the [Predictive HPA guide](../guides/hpa). ## How Thoras finds your HPA The AIScaleTarget references a Deployment directly. It does not reference an HPA or KEDA ScaledObject. Thoras automatically discovers the HPA that targets the same Deployment by looking up HPAs in the same namespace and matching on `scaleTargetRef`. This means you do not need to change your existing HPA or KEDA configuration when adding Thoras. Thoras finds it, reads its configured metrics to know what to collect, and incorporates its desired replica count into scaling decisions, all without modifying the HPA itself. *** ## KEDA [KEDA](https://keda.sh) (Kubernetes Event-Driven Autoscaling) scales workloads based on external event sources: queue depth, Kafka lag, HTTP request rate, and more. **How they work together** KEDA ScaledObjects create and manage standard Kubernetes HPA objects behind the scenes. Thoras treats KEDA-managed HPAs identically to manually created HPAs. It reads KEDA's configured metrics and incorporates KEDA's desired replica count into its own scaling decisions. Two things happen at runtime: 1. **Thoras scales proactively.** When a new forecast is ready, Thoras writes its suggested replica count to the AIScaleTarget. The Thoras operator reads both Thoras's suggestion and the KEDA-managed HPA's current desired replicas, then scales the workload to `max(thoras_suggested, keda_desired)`. 2. **Thoras acts as a floor for KEDA's reactive scaling.** When KEDA's HPA submits a scale request, a Thoras admission webhook intercepts it and ensures the final replica count is at least `max(keda_proposed, thoras_suggested)`. This prevents KEDA from scaling below Thoras's predictive floor during quiet periods. **What Thoras never does** Thoras never modifies KEDA ScaledObjects or the HPA objects KEDA manages. KEDA retains full control over its HPA configuration. Thoras only influences the replica count at the moment a scale decision is executed. **Three complementary layers** With KEDA and Thoras together, you get three scaling layers working in parallel: KEDA scales based on current event backlog, HPA reacts to live resource pressure, and Thoras scales ahead of both based on predicted future demand. Each layer handles a different class of signal, and none replaces the others. *** ## Cluster Autoscaler [Cluster Autoscaler](https://github.com/kubernetes/autoscaler/tree/master/cluster-autoscaler) adds and removes nodes based on whether pods can be scheduled. **How they work together** Thoras and Cluster Autoscaler operate at different layers and do not communicate directly. Thoras works at the pod level; Cluster Autoscaler works at the node level. The integration is through pod resource requests. Cluster Autoscaler decides how many nodes to provision based on the resource requests (`resources.requests`) of pending pods. Thoras keeps those requests accurately sized to actual workload usage through vertical rightsizing. This gives Cluster Autoscaler a more accurate signal: * **Under-specified requests** cause Cluster Autoscaler to underestimate how many nodes are needed, leading to scheduling pressure and delayed scale-out. * **Over-specified requests** waste node capacity and slow down Cluster Autoscaler's scale-in decisions (nodes appear utilized even when workloads are idle). Right-sized requests let Cluster Autoscaler provision fewer, better-utilized nodes, reducing infrastructure cost without sacrificing reliability. *** ## Karpenter [Karpenter](https://karpenter.sh) is a node provisioning tool that bin-packs pods onto nodes and selects optimal instance types automatically. **How they work together** Like Cluster Autoscaler, Karpenter uses pod resource requests to make provisioning decisions. The same principle applies: accurately sized requests from Thoras's vertical rightsizing give Karpenter better bin-packing signals, enabling tighter consolidation and smaller, more cost-effective node selections. Karpenter's consolidation feature (moving pods to fewer nodes) also benefits from right-sized requests. When pods request only what they actually use, Karpenter can consolidate more aggressively without risking resource pressure. Thoras does not configure or modify Karpenter NodePools or EC2NodeClasses. *** ## Summary | Tool | Integration type | Thoras role | | - | - | - | | HPA | Direct | Reads HPA metrics; intercepts scale requests; never modifies HPA | | KEDA | Via HPA | Same as HPA; KEDA-managed HPAs treated identically | | Cluster Autoscaler | Indirect | Right-sized pod requests improve node provisioning signals | | Karpenter | Indirect | Right-sized pod requests improve bin-packing and consolidation | # Frequently Asked Questions Source: https://docs.thoras.ai/faq/introduction ## Where does Thoras store my data? We have no access to your data. Because Thoras is self-hosted, all of your data stays with you and never leaves your environment. All data and metrics remain local to your cluster and are never transmitted beyond your environment's perimeter. ## What type of data does Thoras use? The Thoras platform uses historical numeric Kubernetes metrics to train its models and generate predictions. It collects container performance metrics and custom metrics via the Kubernetes API. ## How long does Thoras take to install? Installation takes less than an hour and is done via a Helm chart. ## How are cost, savings, and waste calculated? Thoras monitors various signals across your cluster to calculate cost, savings, and waste over time: * **Controller spec and pod resource requests**: compute resources such as CPUs, memory, GPUs, etc. allocated to your workloads * **Pod usage and utilization**: what's actually being consumed versus what's requested * **Node-level metrics**: instance types, node counts, and their associated pricing All of this data is stored as time-series metrics within your cluster, so Thoras can track your spend, waste, and savings over time as you optimize with the platform. **Important:** All cost calculations and metric storage happen entirely within your cluster, no data leaves your environment. ## How does Thoras reside in a customer environment? The platform is lightweight and runs on a per-cluster basis to manage target workloads using an `AIScaleTarget` custom resource. The `AIScaleTarget` is configured with just a few lines of YAML. ## Do I have to pick a model to get accurate forecasts? No. Thoras handles all model training and tuning to ensure you receive the most accurate forecasts. It leverages a variety of model architectures, parameters, and features, and continuously scores itself to identify the optimal model for your workload patterns. Users can customize inputs such as prediction frequency and operating mode (e.g., cost-saving, balanced, etc.). ## Where do my forecasting models live? As with all your data, all models reside within your cluster. Training, tuning, and prediction happen entirely within your environment. Models never leave your infrastructure. ## How often do forecasts get made? You're in control. We generally recommend a forecast cadence of 5 to 30 minutes. More variable workloads often benefit from a faster cadence. Thoras automatically manages model training and fine-tuning as needed. It continuously monitors forecast quality and updates models whenever improvements are possible, so no user intervention is required. ## What if my forecast is wrong? Thoras will NEVER leave your services under-provisioned. While even the most accurate forecasting models can't anticipate events like an early marketing email or a DDoS attack, Thoras is built to handle these situations reliably. If your service suddenly experiences unexpected demand, Thoras overrides the forecast and scales in real time to meet current needs. ## How much data does Thoras need to produce accurate forecasts? Most services will receive highly accurate forecasts within the first 48 hours. In rare cases, more time may be needed. For example, if your service has a unique usage pattern that only occurs on Saturdays, Thoras would need to observe at least one Saturday to 'warm up' and learn that specific trend. ## What types of predictive scaling does Thoras provide? For what use cases is each type of scaling method recommended? Thoras provides both predictive horizontal pod scaling (adjusting replica count) and predictive vertical pod rightsizing (adjusting CPU and memory requests). * **Predictive horizontal pod autoscaling** is recommended for workloads with variable traffic patterns that need to scale the number of replicas to handle demand spikes. * **Predictive vertical pod rightsizing** is ideal for workloads with more predictable resource patterns that benefit from optimized CPU and memory requests to improve cluster utilization and reduce costs. ## Can I use both vertical and horizontal modes together? **Recommendation mode (both directions):** You can have both vertical and horizontal in `recommendation` mode simultaneously. Thoras will provide suggestions for both, but these suggestions are mutually exclusive. For example, if Thoras suggests 1Gi memory (vertical) and 3 pods (horizontal), the workload can rightsize by either applying the vertical suggestion or the horizontal suggestion, but not both. **Autonomous mode (one direction only):** Only one scaling direction can be in `autonomous` mode at a time. When enabling autonomous mode, you choose whether Thoras should automatically apply vertical or horizontal scaling. Contact Thoras' support if you wish to discuss which direction is the best option for your workloads. See [Understanding Vertical and Horizontal Scaling Modes](/guides/configuring-asts#understanding-vertical-and-horizontal-scaling-modes) for additional details. ## What types of metrics does Thoras support for vertical and horizontal scaling? Thoras supports CPU, memory, and any custom metrics relevant to your workload. ## Does Thoras support integration with existing autoscalers? Yes. Thoras works alongside HPA, KEDA, Cluster Autoscaler, and Karpenter without replacing or modifying any of them. See [How Thoras integrates with existing autoscalers](./integrations) for a detailed breakdown of each integration. ## Does Thoras support integration with CI/CD? Yes! Thoras is compatible with virtually all CI/CD tools and workflows, including Argo CD, Flux, and Blue/Green deployments. See the [ArgoCD guide](/guides/argo-cd) for the recommended `Application` configuration. We can also add support for any unique or unsupported workflows upon request. ## Is the Thoras dashboard protected? Yes. The dashboard requires a login by default. On first install Thoras generates a password inside your cluster. For production, you can switch to single sign-on against Okta, Azure Entra ID, or any OIDC-compliant identity provider, or disable the built-in layer if you already enforce authentication at your ingress or service mesh. See [Dashboard authentication](/guides/dashboard-auth). ## How does Thoras handle credentials? Any required credentials you don't supply are generated in-cluster on first bootstrap and stored in a Kubernetes Secret. Alternatively, you can manage the secrets Thoras needs with your preferred tooling and hand them to the platform through one of the paths the Helm chart supports. See [Credentials](/installation/credentials). # Quickstart Source: https://docs.thoras.ai/getting-started/quickstart Deploy Thoras and see Kubernetes resource recommendations in minutes! ## Overview This guide will walk you through how to quickly install Thoras and get to see resource recommendations for your workloads in just a few minutes. ## What you'll need 1. Your Thoras license key (email [support@thoras.ai](mailto:support@thoras.ai) if you don't have one) 2. [Helm](https://helm.sh/) 3. Privileged access to a Kubernetes cluster 4. The cluster should have [Kubernetes metrics server](https://github.com/kubernetes-sigs/metrics-server) installed. It probably already has this! For optimal forecast accuracy and full Autonomous Mode support, Thoras needs a Kubernetes Persistent Volume (PV) to store historical metrics. This QuickStart intentionally skips that setup so you can get the product running quickly and start exploring waste insights right away. If you prefer to include the PV during initial installation, review the [Persistent Storage](../installation/persistent-storage) before proceeding with the [Install Thoras](#install-thoras) steps below. You can also always add a PV as a follow-on step. ## Install Let's get Thoras up-and-running! ### Use the Thoras Helm repo Install the Thoras Helm repo, which will give you access to all of the Thoras Helm charts. ``` helm repo add thoras https://thoras-ai.github.io/helm-charts helm repo update thoras ``` ### Install Thoras 1. Note the name of your license key file. We'll use `thoras_license.txt` in our example below. 2. Install Thoras via Helm: ``` helm install thoras thoras/thoras \ --namespace thoras \ --create-namespace \ --set imageCredentials.password="$(cat ./thoras_license.txt)" \ --set featureFlags.enableLegacySecretSeeding=false ``` `featureFlags.enableLegacySecretSeeding` exists only so clusters upgrading from chart 4.x can migrate their existing credentials. A fresh install has nothing to migrate, so turning it off keeps the release clean. See [Upgrading](../installation/upgrading) if you're coming from 4.x. The chart ships with defaults that should work for most initial deployments. See [Advanced Setup](../installation/advanced-setup) for the common configuration knobs, or the [full values reference](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#values) on GitHub. Need to set up license secret externally? See [Advanced Setup](../installation/advanced-setup#reference-an-existing-secret). ### Verify installation It should only take 1-2 minutes for all Thoras pods to come online. Verify that all pods are healthy by confirming their `Running` status: ``` kubectl get pods -n thoras ``` On a fresh install, pods briefly report `CreateContainerConfigError` while the Thoras config-controller generates the credentials they need. This resolves on its own within a minute. No action is needed. Once all pods are in `Running` status, congratulations! You've successfully installed Thoras! ## Get Workload Resource Recommendations Now that Thoras is up and running, we're just minutes away from getting resource recommendations that will help our workloads run more efficiently, boosting reliability and saving on cloud costs. ### View the dashboard We'll be working out of the Thoras Dashboard from here on out. To access it: 1. Port-forward to the dashboard pod: ``` kubectl port-forward service/thoras-dashboard -n thoras 8080:80 ``` 2. Pull up the dashboard in your browser: [http://localhost:8080](http://localhost:8080) 3. The dashboard prompts for a username and password. The default username is `thoras`. Fetch the generated password with: ``` kubectl get secret thoras-config-controller -n thoras \ -o jsonpath='{.data.dashboard-auth-password}' | base64 -d ``` See [Dashboard authentication](/guides/dashboard-auth) for how to change the credentials, sign in with your identity provider (Okta, Azure Entra ID), or bring your own auth. Before exposing the dashboard beyond `localhost`, terminate TLS at your ingress and set `thorasDashboard.auth.cookieSecure: true`. Thoras will auto-discover workloads in your cluster and give you the option to onboard all the workloads in a namespace, with zero configuration needed. This is a great way to experiment and see the value of Thoras quickly! 4. Select a namespace from the dropdown and click "Enroll Namespace" to enroll all associated workloads, or alternatively select individual workloads from the table below and click "Enroll Selected" to specify the workloads you'd like to target. This will enroll workloads into `Recommendation Mode` so they may begin receiving resource recommendations. This is a read-only operation. No changes will take place to your workloads. auto-create 5. You'll see a new popup where Thoras tells you it's populating `AIScaleTargets` for you. 6. Once complete, you'll see a list of onboarded targets under the Recommendation tab on the Home page. Click "View" to see more! live-targets 7. You should immediately see metrics beginning to collect! Use the slider below the graph to narrow the time window. metrics-collecting 8. Predictions will run on a 15m interval. Grab a cup of coffee! ☕ ### Replica and resource recommendations Predictions will become more accurate over time as the Thoras Reasoning Engine learns more about your environment. But we can still get useful predictions even on the first forecast! Let's dive in. #### Workload efficiency at a glance The `Pod Health` widget will give you a quick overview of replica count recommendations and workload's overall resource utilization. pod-health #### Pod resource recommendations Check out how much more efficiently, reliably, and cost-effectively your workloads could be running in the `Resource Allocation Settings` widget. resource-allocation ## Getting support For any questions, email [support@thoras.ai](mailto:support@thoras.ai). # What is Thoras? Source: https://docs.thoras.ai/getting-started/what-is-thoras The autonomous control plane for compute efficiency. Thoras is a self-hosted, AI/ML-powered platform that replaces reactive Kubernetes autoscaling with predictive, autonomous resource management. It eliminates waste, prevents capacity outages, and safely optimizes critical infrastructure. Thoras is designed to earn your trust before running autonomously. Reduction in cloud compute waste from CPU and memory over-provisioning. OOM kill loops resolved automatically. Latency spikes prevented through rightsized compute. Continuous right-sizing without manual tuning or on-call escalations. ## Philosophy **Autonomy is earned, not assumed.** Thoras doesn't arrive in your cluster and start making decisions. It observes, learns, surfaces recommendations, and only acts autonomously once you've validated its reasoning. Your workload and infrastructure context are always respected. **Performance first, efficiency second.** Thoras reclaims waste from under-utilized CPU and memory without compromising availability. It knows when not to scale down as clearly as it knows when to scale up. ## How it works Thoras ingests real-time telemetry from your existing metric sources (Prometheus, the Kubernetes metrics server, and any custom or external metrics you already use) and persists historical usage data inside your cluster. It builds per-workload demand profiles that account for seasonality, traffic patterns, and resource trends. AI/ML models continuously predict horizontal and vertical scaling needs ahead of time. Forecasts update on configurable intervals (typically minutes to hours), with model accuracy improving as patterns repeat. Before any autonomous action, Thoras surfaces predictions and right-sizing recommendations in a dashboard. Teams validate model accuracy first. Autonomous mode requires 24 hours of historical data by default, and benefits from at least 48 hours for workloads with daily seasonality. Once enabled, Thoras scales pods and replica counts in advance of demand, remediates OOM kill loops in real time with compounding memory adjustments, and maintains configurable upper and lower bounds at all times. You can pause autonomous scaling cluster-wide at any moment. ## Architecture Thoras installs as a single Helm chart and runs entirely inside your cluster. Nothing leaves your environment unless you opt in to cloud sync. The platform is lightweight (a small install runs on about 2 vCPU) and consists of the following components: | Component | Role | | - | - | | **Operator** | Reconciles `AIScaleTarget` resources and applies scaling decisions. Hosts the admission webhooks that keep other autoscalers from undoing Thoras's work. | | **Metrics collector** | Gathers pod and node usage into an embedded TimescaleDB (or a database you manage) so models have history to learn from. | | **Forecast workers** | Train and run the ML models. The pool scales itself as you enroll more workloads. | | **Worker** | Background jobs: cost refresh, monitors, OOM remediation, and reconciliation. | | **API server** | Serves forecasts, recommendations, and cost data to the dashboard and to other components, using shared bearer-token authentication. | | **Dashboard** | The web UI, fronted by an authentication sidecar (generated credentials by default, or single sign-on via your OIDC identity provider). | | **Config-controller** | Seeds the credentials you don't supply, keeps them in sync, and performs dependency-ordered restarts when they change. | See [Advanced Setup](/installation/advanced-setup) for how to configure each component. ## Capabilities Pre-emptive CPU and memory request optimization. Day-one support for K8s 1.33+ in-place pod resize, with rolling restart fallback. Replica counts adjusted ahead of demand spikes, not after. Integrates with your existing HPA. Detects kill loops and stabilizes workloads with compounding memory adjustments (1.2× per cycle) until the forecaster catches up. Purpose-built handling for Java workloads, with heap and GC pressure awareness. Real-time waste quantification against node pricing data. Savings estimated per workload. `ClusterAIScaleTemplate` applies scaling policies across namespaces without per-workload config. ## Get started Install Thoras and see resource recommendations in minutes. How Thoras works alongside KEDA, Cluster Autoscaler, and Karpenter. # ArgoCD Source: https://docs.thoras.ai/guides/argo-cd ArgoCD is a declarative, GitOps continuous delivery tool for Kubernetes applications. This guide covers deploying Thoras with ArgoCD and managing Thoras-targeted workloads alongside it. ## Using ArgoCD to deploy Thoras The Thoras Helm chart is designed to render deterministically for GitOps tools. Credentials are never generated at template time: anything you don't supply is seeded in-cluster by the Thoras config-controller, and anything you pin in `values.yaml` is rendered verbatim. Two things still need attention: 1. **Disable legacy secret seeding.** Set `featureFlags.enableLegacySecretSeeding: false`. The flag defaults to `true` only so clusters upgrading from chart 4.x can migrate their pre-existing credentials. The legacy Secrets it renders use Helm `lookup`, which Argo CD cannot evaluate. If you are upgrading from 4.x, complete the upgrade with the flag on, then set it to `false` once the pods are healthy. During that upgrade window, keep `jsonPointers: [/data]` `ignoreDifferences` entries for the `api-client-secret` and `thoras-timescale-password` Secrets so Argo CD does not overwrite them with freshly-rendered random values before the config-controller has migrated them. Remove those entries once the flag is `false`. See [Upgrading](/installation/upgrading). 2. **Ignore fields that are managed in-cluster.** The webhook `caBundle` is injected after apply (by the chart's cert-generator or by cert-manager), and the `thoras-forecast-worker` replica count is managed by Thoras's built-in scaler. Add both to `spec.ignoreDifferences`. ### Example Application config ```yaml theme={null} apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: thoras namespace: argocd spec: project: default source: chart: thoras helm: parameters: - name: imageCredentials.password value: - name: featureFlags.enableLegacySecretSeeding value: "false" repoURL: https://thoras-ai.github.io/helm-charts targetRevision: destination: namespace: thoras server: https://kubernetes.default.svc syncPolicy: automated: {} ignoreDifferences: # The webhook caBundle is injected in-cluster after apply. - group: admissionregistration.k8s.io kind: MutatingWebhookConfiguration jqPathExpressions: - ".webhooks[]?.clientConfig.caBundle" - group: admissionregistration.k8s.io kind: ValidatingWebhookConfiguration jqPathExpressions: - ".webhooks[]?.clientConfig.caBundle" # Forecast worker replicas are managed by Thoras at runtime. - jsonPointers: - /spec/replicas kind: Deployment name: thoras-forecast-worker ``` The chart's default webhook certificate flow uses Helm hook Jobs (`pre-install`, `pre-upgrade`), which GitOps tools do not handle well. If you run [cert-manager](https://cert-manager.io), set `thorasOperator.webhookCertGen.certManager.enabled: true` to replace those Jobs with declarative `Issuer` and `Certificate` resources. See [Webhook TLS with cert-manager](/installation/advanced-setup#webhook-tls-with-cert-manager). ## Credential rotation under GitOps When a credential changes (you rotate a pinned value in `values.yaml`, update a Secret you manage, or delete a generated key so it is re-seeded), the config-controller restarts the affected Thoras workloads for you. It does this by evicting pods rather than patching the Deployment's pod template, so the restart never shows up as drift in Argo CD. No `kubectl rollout restart` is needed. See [Credentials](/installation/credentials). ## Flux The same guidance applies to Flux: set `featureFlags.enableLegacySecretSeeding: false` and prefer cert-manager for webhook TLS. Additionally set `flux.enabled: true` so Flux's drift detection does not revert the operator's own rollout restarts: ```yaml theme={null} flux: enabled: true ``` ## Using ArgoCD to manage a Thoras horizontally controlled workload When Thoras manages horizontal scaling for a workload, it uses a validation webhook to prevent the default Kubernetes Horizontal Pod Autoscaler (HPA) from reducing replicas during anticipated usage spikes. By design, this ensures workloads remain scaled up until Thoras determines it's safe to scale down. However, ArgoCD interprets these blocked scale-down attempts as HPA errors. This can cause the Application to appear in a degraded state even though the workload is healthy. To prevent this, you can customize ArgoCD's health assessment logic for HPAs so that these Thoras-specific conditions are handled correctly. ### When Do I Need This? You only need this customization if all of the following are true: * The workload is horizontally controlled by Thoras. * You are using ArgoCD to monitor the workload state. * You notice ArgoCD incorrectly marking the HPA as **Degraded** when Thoras blocks a scale-down. If your workloads are **not** using Thoras for horizontal scaling, this customization is unnecessary. **1. Edit the ArgoCD ConfigMap**: Open the `argocd-cm` ConfigMap in the `argocd` namespace: ```bash theme={null} kubectl edit configmap argocd-cm -n argocd ``` **2. Add the following customizations under** `data`: ``` resource.customizations.useOpenLibs.autoscaling_HorizontalPodAutoscaler: "true" resource.customizations.health.autoscaling_HorizontalPodAutoscaler: | hs = {} if obj.status ~= nil then if obj.status.conditions ~= nil then for i, condition in ipairs(obj.status.conditions) do -- Mark Degraded only for certain conditions if condition.type == "Ready" and condition.status == "False" then hs.status = "Degraded" hs.message = condition.message return hs end -- Ignore AbleToScale=False with FailedUpdateScale reason caused by Thoras if condition.type == "AbleToScale" and condition.status == "False" and condition.reason == "FailedUpdateScale" and string.find(condition.message or "", "target is already at Thoras desired replicas") ~= nil then hs.status = "Healthy" hs.message = "Target is at Thoras desired replicas" return hs end -- Mark Healthy for Ready=True or ScalingActive=True if (condition.type == "Ready" or condition.type == "ScalingActive") and condition.status == "True" then hs.status = "Healthy" hs.message = condition.message return hs end end end end hs.status = "Progressing" hs.message = "Waiting for HPA to become healthy" return hs ``` **3. Restart ArgoCD components** (to apply changes): ```bash theme={null} kubectl rollout restart deployment argocd-repo-server -n argocd kubectl rollout restart deployment argocd-applicationset-controller -n argocd ``` # Audit Trail Source: https://docs.thoras.ai/guides/audit-trail See who changed scaling behavior, and what changed, from the api-server logs The api-server logs an audit event for every privileged action it takes: changing a target's scale mode, pausing or resuming scaling cluster-wide, cleaning up a removed target, and enrolling new targets. Each event records who took the action, what changed, and whether it succeeded. ## What's recorded | Action | Trigger | | - | - | | `ast.mode.update` | Changing a target's horizontal or vertical scale mode | | `system.scaling.update` | Pausing or resuming [autonomous scaling](/guides/pause-scaling) cluster-wide | | `ast.cleanup` | Cleaning up a removed target's metrics | | `ast.enroll_candidates` | Enrolling workloads in a namespace (all of them, if none are named) | | `ast.enroll_all` | Enrolling all namespaces | Both successful and failed attempts are recorded. A dry-run or count-only enrollment preview isn't, since nothing is actually changed. Not every dashboard-reachable change goes through this list yet — cost-savings settings is the one left out of this first pass. ## Who's recorded The dashboard forwards the identity of the person who took the action, but only when [dashboard authentication](/guides/dashboard-auth) can establish one: | Auth mode | Actor recorded | | - | - | | `oidc` | The user's email | | `htpasswd` | The shared username | | `auth.enabled: false`, or any direct API call | `unknown` | If per-person attribution matters to you, use `oidc` mode. `htpasswd` and disabled auth both share one identity across everyone with dashboard access. The actor is an attribution hint, not authentication. Anyone holding your `apiClientSecret` can call the API directly and set it themselves. It's useful for distinguishing a change made through the dashboard from one made by a script or another integration, not for proving who made it. ## Reading the log ```bash theme={null} kubectl logs -n thoras deploy/thoras-api-server-v2 | grep "audit event" ``` Each line carries these fields: | Field | Meaning | | - | - | | `actor`, `actor_source` | Who took the action, and how that was determined | | `action` | One of the values above | | `target_kind`, `target_namespace`, `target_name` | What was acted on | | `old_value` | The state before the action, for actions that have one; not every action does | | `new_value` | The result, set only on success. Absent on a failed attempt — `details.requested` has what was asked for instead | | `outcome` | `success` or `failure` | | `error` | Present only on failure | | `persisted` | Whether this event also made it into the database (see below); `false` means only this log line exists | | `details` | Extra context specific to the action — a count, a request ID, or what was asked for versus what changed | Pod logs are subject to your cluster's log retention, so ship api-server logs to your log aggregator if you need to keep this history longer than that. ## Is there a durable copy? Yes — every event is also written to a database table before this log line is emitted, so a log rotation doesn't lose the history. That table isn't a supported, documented interface yet: there's no API or dashboard view onto it, its schema isn't stable across releases, and support for querying it directly isn't something we can commit to today. Treat the log line above as the supported way to read this trail; `persisted: false` on a line means even the database copy is missing for that one event, so the log was all there was. # AWS Cost Explorer Source: https://docs.thoras.ai/guides/aws-cost-explorer Collect your cluster's real billed AWS spend so Thoras reports actual cost instead of estimates. Thoras estimates cluster cost from a built-in static price list. Those estimates are directional by design — they don't know about your reserved instances, savings plans, or negotiated rates. See [Cost Estimates](/guides/cost-estimates) for how that calculation works. Enabling AWS Cost Explorer collection gives Thoras the spend AWS actually billed you, broken down by service and usage type for each day. Cost reporting uses this in place of the estimates, so the figures you see match your invoice. Collection is off by default. AWS bills every Cost Explorer API request, so turning it on is an explicit opt-in. Cost Explorer collection reads AWS billing, so it applies to EKS clusters. Thoras skips collection on clusters it detects are running in another cloud rather than paying for a query that cannot return anything. ## Before you start You need: * An EKS cluster with an [IAM OIDC provider](https://docs.aws.amazon.com/eks/latest/userguide/enable-iam-roles-for-service-accounts.html) associated, so the Thoras worker can assume a role through IRSA. * [Cost Explorer enabled](https://docs.aws.amazon.com/cost-management/latest/userguide/ce-enable.html) on the account that pays for the cluster. * The `aws:eks:cluster-name` [cost allocation tag activated](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/activating-tags.html) in the Billing console of that account. Activating the cost allocation tag is the step most often missed. Thoras filters your account's spend down to this cluster using that tag, and Cost Explorer only returns tagged spend for tags that have been activated. If the tag is inactive, every query succeeds and returns nothing. Activation can take up to 24 hours to take effect, and tag data may not reach all the way back through the initial 90-day backfill window. ## Grant access to Cost Explorer The Thoras worker reaches Cost Explorer with whatever AWS credentials its pod picks up. On EKS, bind an IAM role to the worker's service account with IRSA. The role needs a single permission, `ce:GetCostAndUsage`. Cost Explorer does not support resource-level permissions, so the policy is scoped to `*`. Thoras also resolves your AWS account ID through `sts:GetCallerIdentity`, which requires no permission of its own. ### Terraform ```hcl theme={null} data "aws_eks_cluster" "this" { name = var.cluster_name } # Your cluster's existing IAM OIDC provider. Create one with # aws_iam_openid_connect_provider first if the cluster does not have it. data "aws_iam_openid_connect_provider" "eks" { url = data.aws_eks_cluster.this.identity[0].oidc[0].issuer } locals { # Trust policy conditions are keyed on the issuer host, not the full URL. oidc_host = replace( data.aws_eks_cluster.this.identity[0].oidc[0].issuer, "https://", "", ) } data "aws_iam_policy_document" "thoras_cost_explorer_trust" { statement { effect = "Allow" actions = ["sts:AssumeRoleWithWebIdentity"] principals { type = "Federated" identifiers = [data.aws_iam_openid_connect_provider.eks.arn] } # Only the Thoras worker's service account may assume this role. Match # thorasWorker.serviceAccount.name if you changed it from the default. condition { test = "StringEquals" variable = "${local.oidc_host}:sub" values = ["system:serviceaccount:${var.thoras_namespace}:thoras-worker"] } condition { test = "StringEquals" variable = "${local.oidc_host}:aud" values = ["sts.amazonaws.com"] } } } data "aws_iam_policy_document" "thoras_cost_explorer" { statement { effect = "Allow" actions = ["ce:GetCostAndUsage"] # Cost Explorer has no resource-level permissions. resources = ["*"] } } resource "aws_iam_role" "thoras_cost_explorer" { name = "thoras-cost-explorer" assume_role_policy = data.aws_iam_policy_document.thoras_cost_explorer_trust.json } resource "aws_iam_role_policy" "thoras_cost_explorer" { name = "thoras-cost-explorer" role = aws_iam_role.thoras_cost_explorer.id policy = data.aws_iam_policy_document.thoras_cost_explorer.json } ``` Activate the cost allocation tag in the same run. This resource belongs in the account that pays the bill, which may be a different provider alias than the one holding the cluster: ```hcl theme={null} resource "aws_ce_cost_allocation_tag" "eks_cluster_name" { tag_key = "aws:eks:cluster-name" status = "Active" } ``` ### Without Terraform Create a role with this trust policy, substituting your account ID, OIDC issuer host, and the namespace Thoras is installed in: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:thoras:thoras-worker", "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com" } } } ] } ``` Attach this permission policy to it: ```json theme={null} { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "ce:GetCostAndUsage", "Resource": "*" } ] } ``` ## Enable collection Point the worker's service account at the role and turn the collector on: ```yaml theme={null} thorasWorker: serviceAccount: annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/thoras-cost-explorer enableCostAndUsageCollector: true ``` Then `helm upgrade` your release. The worker restarts and begins collecting immediately. ## How Thoras identifies your cluster's spend Your AWS account bills for more than one cluster, so Thoras narrows each query to a cost allocation tag. It reads that tag from your node labels: | Node label | Cost allocation tag | | - | - | | `eks.amazonaws.com/cluster-name` | `aws:eks:cluster-name` | | `alpha.eksctl.io/cluster-name` | `aws:eks:cluster-name` | If none of your nodes carry those labels — self-managed nodes often don't — collection stops and logs the labels it looked for. Set the tag yourself in that case: ```yaml theme={null} thorasWorker: costExplorerFilter: "my-cost-tag=prod-cluster" ``` The value is a `TAG_KEY=TAG_VALUE` pair and replaces the node label lookup entirely. Use it when your spend is tracked under your own tagging scheme rather than the AWS-generated cluster tag. Whatever key you choose must be activated as a cost allocation tag, the same as `aws:eks:cluster-name`. ## What Thoras collects Each run stores one row per day, per AWS service, per usage type: * **Billed cost** — the cost as it appears on your invoice. * **Effective cost** — amortized and commitment-adjusted, so reserved instance and savings plan fees are spread across the term they cover rather than landing on the day they were charged. * **Usage quantity and unit** — how much of that usage type was consumed. The collector runs every 24 hours. AWS finalizes a day's costs well after the day ends, so collecting more often would not make the data fresher. * The first run backfills **90 days**. * Every run after that re-fetches the **last 7 days**, overwriting what it already stored. AWS restates recent days as usage finalizes, and this picks up those corrections. * The job also runs when the worker starts, but skips if the stored data is less than 20 hours old, so a rollout or a crash loop doesn't pay for the same days again. ## What this costs AWS charges per Cost Explorer API request — \$0.01 per paginated request as of September 15, 2026. See [AWS Cost Management pricing](https://aws.amazon.com/aws-cost-management/pricing/) for current rates. A nightly run costs a handful of requests. The 90-day backfill on first run is larger but one-time. Cost scales with how many distinct service and usage type combinations your account bills for, since that determines how many pages each query returns. ## Verify it's working Check the worker logs: ```bash theme={null} kubectl logs -n thoras deploy/thoras-worker | grep -i "cost and usage" ``` On startup you should see `Cost and usage collector is enabled`, then `Collecting daily cost and usage` with the date window and the tag Thoras resolved: ``` Collecting daily cost and usage start=2026-06-17 end=2026-09-14 tagKey=aws:eks:cluster-name tagValue=prod-cluster ``` Confirm `tagValue` matches your cluster. If it doesn't, set `costExplorerFilter` explicitly. ## Troubleshooting **`Cost and usage collection returned no rows`** — the query succeeded but matched no billed resources. Almost always the cost allocation tag isn't activated, or the `tagKey` and `tagValue` in the log don't match how your resources are actually tagged. Check the Billing console first, then verify the tag against a node in the EC2 console. **`no cost filter could be resolved`** — none of your nodes carry a recognized cluster name label. The log lists the labels Thoras searched for. Set `thorasWorker.costExplorerFilter` to your own `TAG_KEY=TAG_VALUE` pair. **`AccessDeniedException` when calling Cost Explorer** — the worker assumed a role without `ce:GetCostAndUsage`, or didn't assume one at all. Confirm the service account carries the annotation: ```bash theme={null} kubectl get serviceaccount thoras-worker -n thoras -o yaml ``` If the annotation is there but the role isn't being assumed, the trust policy's `sub` condition likely doesn't match — it must name the namespace Thoras is installed in and the worker's service account name. **`Could not build the AWS Cost Explorer provider`** — the worker couldn't read its AWS configuration. It keeps running and collects nothing rather than failing startup. Check for a malformed AWS config file or environment. **`The cost and usage provider does not bill the cloud this cluster runs in`** — Thoras detected the cluster is running somewhere other than AWS and skipped the query. Setting `costExplorerFilter` bypasses this check if you're intentionally attributing non-EKS spend. # Configuring AIScaleTargets Source: https://docs.thoras.ai/guides/configuring-asts To enable Thoras to manage or recommend scaling for your workload, you must register an `AIScaleTarget` Custom Resource (CRD) in your Kubernetes cluster. This guide will walk you through doing this by applying an `AIScaleTarget` resource with `kubectl`. #### Prerequisites * A running Kubernetes cluster with [Kubernetes metrics server](https://github.com/kubernetes-sigs/metrics-server) installed * Thoras installed and running (see [quickstart guide](../getting-started/quickstart)) #### 0. (Optional) Auto-configure `AIScaleTarget`s using the dashboard On a fresh installation, Thoras can auto-discover workloads and make suggestions to auto-populate `AIScaleTarget`s in the cluster. This enables metric collection and resource recommendations for the selected workloads (see [quickstart guide](../getting-started/quickstart#view-the-dashboard)). Eventually you will probably want to integrate your `AIScaleTarget` configurations into your delivery pipelines, but the auto-populate feature is a particularly useful way to try out the product without having to manage any configuration. #### 1. Define Initial `AIScaleTarget` Custom Resource Create a new file named `my-ast.yaml` ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} spec: model: forecast_blocks: 4h forecast_interval: 1h mode: balanced scaleTargetRef: kind: Deployment name: {{YOUR_AST_NAME}} horizontal: mode: recommendation ``` * `metadata.name`: The name for your `AIScaleTarget` resource (should match your workload name for clarity). * `metadata.namespace`: The namespace where your workload is running. * `spec.scaleTargetRef`: Points to the Kubernetes resource (usually a Deployment) you want to scale. * `spec.model.forecast_blocks`: How far into the future to forecast. Thoras sizes for the predicted peak within this window. * `spec.model.forecast_interval`: How often to make a forecast. #### 2. Apply the `AIScaleTarget` Using the CLI Use `kubectl apply` to register the `AIScaleTarget` with your cluster: ```sh theme={null} kubectl apply -f my-ast.yaml ``` You should see output like: ```sh theme={null} aiscaletarget.thoras.ai/my-name created ``` Replacing an AIScaleTarget that existed before? Create the new one with the `thoras.ai/inherit-history: "true"` annotation to [keep the earlier history](/reference/ast-definition#keeping-history-when-an-ast-is-recreated). ### 3. Verify the `AIScaleTarget` Check that your `AIScaleTarget` was created: ```sh theme={null} kubectl get aiscaletarget -n my-namespace ``` To see details: ```sh theme={null} kubectl describe aiscaletarget my-app -n my-namespace ``` Note: An existing `AIScaleTarget` can be exported to a YAML for version control or editing with: ```sh theme={null} kubectl get aiscaletarget -n -o yaml > my-ast.yaml ``` ## Understanding Vertical and Horizontal Scaling Modes An `AIScaleTarget` can have both `vertical` and `horizontal` sections configured, each with its own `mode` setting (`recommendation` or `autonomous`). However, it is important to understand how these interact. ### Recommendation Mode (Both Directions) When both vertical and horizontal are in `recommendation` mode, Thoras provides suggestions for both scaling directions. These suggestions are mutually exclusive, and should be treated as **one or the other**. For example, if Thoras suggests 1Gi memory (vertical) and 3 pods (horizontal), the workload could be rightsized by either: * Applying the vertical suggestion (1Gi memory), **OR** * Applying the horizontal suggestion (3 pods) Applying both suggestions would result in an over-provisioned workload. ### Autonomous Mode (One Direction Only) Only one scaling direction can be in `autonomous` mode at a time. When enabling autonomous mode, you choose whether Thoras should automatically scale vertically or horizontally. Valid configurations: | Horizontal Mode | Vertical Mode | Valid | | - | - | - | | `recommendation` | `recommendation` | ✓ | | `autonomous` | `recommendation` | ✓ | | `recommendation` | `autonomous` | ✓ | | `autonomous` | `autonomous` | ✗ | When enrolling a workload into autonomous mode, consider which scaling direction best fits your workload: * **Horizontal autonomous** is recommended for workloads with spiky usage that require rapid scaling to handle traffic bursts. * **Vertical autonomous** is ideal for workloads that prioritize efficient cluster bin packing and cost optimization. Contact Thoras' support with any questions about which scaling direction is the best option for your workloads and business needs. ## Using Label Selectors to Target Multiple Workloads Instead of targeting a single workload by name with `scaleTargetRef`, you can use `selector` to apply the same vertical scaling policy to multiple workloads that share common pod labels. ### When to Use Label Selectors Label selectors are useful when you want to: * Scale a workload that uses a blue/green deployment strategy * Scale workloads dynamically based on labels without hardcoding specific names * Manage scaling configurations at a higher level of abstraction ### Example: Targeting Multiple Workloads by Label Create an `AIScaleTarget` using a label selector: ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: production-services namespace: {{YOUR_NAMESPACE}} spec: selector: matchLabels: app: my-service environment: production model: forecast_blocks: 4h forecast_interval: 1h vertical: containers: - name: {{CONTAINER_NAME}} cpu: lowerbound: 20m upperbound: 1 memory: lowerbound: 50Mi upperbound: 2G mode: recommendation update_policy: update_mode: in_place_or_recreate ``` This configuration targets all Deployments, StatefulSets, and Rollouts in the namespace whose pods have both `app: my-service` and `environment: production` labels. **Note:** The `containers[].name` field supports wildcard patterns (e.g., `*`, `app-*`) to match multiple containers with a single configuration. See [Container Name Wildcard Matching](/reference/ast-definition#container-name-wildcard-matching) for examples and matching rules. ### Important Constraints * `selector` and `scaleTargetRef` are mutually exclusive, use one or the other * **Horizontal scaling is not supported** with `selector`: omit `spec.horizontal` entirely * Only **vertical scaling** is supported when using label selectors * All targeted workloads must be in the same namespace as the `AIScaleTarget` * Pods must be managed by a Deployment, StatefulSet, or Argo Rollout. Standalone pods or other controller types are not supported. ### Behavior in Autonomous Mode When using `selector` in autonomous mode: 1. Thoras identifies all matching pod controllers in the namespace 2. When the forecaster suggests new resource requests, Thoras updates each matching controller one by one 3. If `update_policy.update_mode` is set to `in_place` or `in_place_or_recreate`, pods are resized in place if possible (Kubernetes 1.33+) 4. If `in_place_or_recreate` is specified and resizing is not possible or fails, then pods are recreated with the new resource requests. If `in_place` is specified and resizing is not possible or fails, then pods are not created but any future pods will be right sized. Additionally, sometimes it can be useful to sync your workload's active resource requests with the manifests in your source code repo to keep the baseline workload definition aligned with reality. Email [support@thoras.ai](mailto:support@thoras.ai) to learn how to automate this. # Cost Estimates Source: https://docs.thoras.ai/guides/cost-estimates Understand how Thoras calculates potential savings and waste across your Kubernetes cluster based on workload resource usage and node pricing data. Thoras estimates cost savings and waste by analyzing your workload resource requests against predicted usage patterns. This guide explains how these calculations work and how to customize pricing data for your environment. ## How Cost Calculations Work Thoras calculates potential savings by comparing your current resource allocations with forecasted recommendations. The calculation involves three main components: ### 1. Determining Average Node Cost Thoras establishes a baseline "average node cost per hour" for your cluster: 1. **Node Discovery** - Collects node information similar to the output from `kubectl get nodes -oyaml` 2. **Metadata Extraction** - Parses labels to identify: * Cloud provider (AWS, GCP, Azure, etc.) * Instance type (e.g., `m7g.medium`, `n2-standard-4`) * Capacity type (on-demand or spot) 3. **Price Matching** - Matches node details against pricing data to determine hourly cost, CPU cores, and memory 4. **Cluster Averaging** - Averages cost, cores, and memory across all nodes to establish cluster-wide baseline If pricing data is unavailable for a node (e.g. unknown instance type), that node is excluded from the cluster's "average node." If no nodes can be priced, Thoras defaults to 4 cores, 16GB memory, and \$0.19/hour (equivalent to an AWS `t3.xlarge`). **Note:** This approach provides ballpark estimates rather than precise billing figures. The goal is directional guidance on how rightsizing workloads impacts your overall cloud spend. On EKS, you can have Thoras collect your real billed spend instead — see [AWS Cost Explorer](/guides/aws-cost-explorer). ### 2. Calculating Workload-Level Savings For each workload, Thoras compares original controller specifications (e.g. Deployment, StatefulSet, ArgoRollout) against forecasted resource recommendations: **Over-provisioned workloads** (positive delta): * Current requests exceed predicted usage * Labeled as **waste** in recommendation mode * Labeled as **savings** in autonomous mode **Under-provisioned workloads** (negative delta): * Current requests fall below predicted usage * Tracked as **Resources Needed** (recommendation mode) or **Resources Added** (autonomous mode) * Thoras recommends increasing resources to improve stability Thoras tracks the potential reduction (or required increase) in CPU cores and memory bytes per hour for each workload. ### 3. Cluster-Wide Cost Impact Thoras aggregates workload-level changes across your entire cluster: 1. Sum wasted or saved CPU and memory across all workloads 2. Calculate how many fewer nodes are needed based on: * Aggregate CPU core reduction * Aggregate memory reduction 3. Use the **lower of the two** (CPU or memory determines node count) 4. Multiply by average node hourly cost to estimate total cluster savings This represents the maximum potential savings if all right sizing recommendations were applied autonomously by Thoras. ## Sourcing Pricing Data Because Thoras operates in an air-gapped environment without real-time cloud API access, pricing data comes from a built-in static price list that is periodically updated in product releases. ### Handling Spot Pricing Spot instance pricing fluctuates based on supply and demand. Rather than tracking real-time spot prices, Thoras uses published average savings percentages from cloud providers. For example, according to [AWS EC2 Spot Instance Advisor](https://aws.amazon.com/ec2/spot/instance-advisor/), a spot `m7g.medium` provides an average 59% savings over on-demand pricing (as of February 5, 2026). **Note:** Your actual spot costs may differ from dashboard estimates because Thoras uses static averages rather than current market rates. ### Preemptible Instances Preemptible instances (GCP's predecessor to their spot offering) are priced using spot pricing data as a proxy. While not exact, the two pricing models are typically close. Your actual preemptible costs may vary from dashboard estimates. ## Overriding Average Node Cost You can manually override the calculated average node cost when: * Your cluster uses an bespoke instance types * You have custom pricing agreements or reserved instance discounts * You want more precise cost calculations based on your actual billing To override the average node cost, navigate to the cluster settings in the Thoras dashboard and enter your custom value. ## What's Included (and What's Not) **Included in cost calculations:** * CPU and memory resource costs * Node-level infrastructure pricing **Not included in cost calculations:** * Reserved instance or savings plan discounts * Committed use discounts (GCP) * Custom pricing agreements For workloads with significant non-compute costs, treat Thoras savings estimates as a lower bound on total potential savings. ### Horizontal Scaling Savings and Waste Vanilla HPA is reactive by design. To protect against sudden traffic spikes, teams typically run workloads at 20-30% utilization. Thoras's predictive horizontal scaling allows workloads to run at much higher instantaneous utilization (80-90%) by scaling proactively based on forecasts. However, Thoras uses vanilla HPA as a safety backstop, always scaling to the higher value between the Thoras forecast and vanilla HPA's recommendation. Realizing savings from this improved utilization requires increasing HPA's target utilization metric, which is a manual configuration change. Because savings depend on customer action to adjust HPA settings, cost estimates exclude horizontal scaling savings. ## Mixed Instance Types Thoras handles heterogeneous clusters gracefully: * **Mixed capacity types** - Averages across spot and on-demand instances * **Multiple instance types** - Blends pricing across different machine types * **Multi-cloud** - Averages across cloud providers if nodes span multiple clouds **Note:** This averaging approach ignores bin-packing optimizations that tools like Karpenter might achieve. Actual savings may be higher if your autoscaler can consolidate workloads onto fewer nodes. ## Node Detail Collection Thoras snapshots node metrics every 5 minutes and rolls up aggregates hourly and daily. Cost projections are based on these aggregated usage patterns. # Dashboard authentication Source: https://docs.thoras.ai/guides/dashboard-auth How the Thoras dashboard authenticates users, and how to configure the two built-in modes: generated htpasswd credentials or OIDC login against your identity provider. ## Overview The Thoras dashboard ships with authentication enabled by default. Requests hit an [oauth2-proxy](https://oauth2-proxy.github.io/oauth2-proxy/) sidecar in the dashboard pod. Only authenticated requests reach the dashboard container. Two modes are supported, selected via `thorasDashboard.auth.mode`: * **`htpasswd`** (default): a username and password generated in-cluster on first install by the Thoras config-controller. No external setup is required. * **`oidc`**: OIDC login against your identity provider (Okta, Azure Entra ID, or any OIDC-compliant provider). You provide the client credentials in a Secret you manage. Only `oidc` gives you per-person attribution in the [audit trail](/guides/audit-trail); `htpasswd` shares one identity across everyone with dashboard access. ``` User -> oauth2-proxy sidecar -> Thoras Dashboard (loopback) ^ htpasswd file OR OIDC IdP ``` Fields under the unused mode's block are ignored, so you can switch modes by changing `auth.mode`. Credentials and session cookies are only meaningful behind TLS. Terminate TLS at your ingress before exposing the dashboard beyond `kubectl port-forward`, and set `thorasDashboard.auth.cookieSecure: true`. The chart default is `false` so that port-forward works over plain HTTP. This default is suitable for evaluation only. Safari drops Secure cookies over plain HTTP, so you cannot enable `cookieSecure` and use `kubectl port-forward` together from a Mac. ## Pick a mode * [Default (htpasswd) auth](/guides/dashboard-auth/htpasswd): log in with the generated credentials, set your own, or supply your own Secret. * [Okta](/guides/dashboard-auth/okta): OIDC login against Okta. * [Azure Entra ID](/guides/dashboard-auth/entra): OIDC login against Entra. Other OIDC-compliant providers work with `provider: oidc`. The Okta and Entra guides show the general shape. ## Migrating from a hand-rolled oauth2-proxy sidecar Before chart 5.0, the documented way to protect the dashboard was to add your own oauth2-proxy container under `thorasDashboard.extraContainers` and retarget the Service at port `4180`. The chart now ships that sidecar for you. Migrating keeps your IdP app registration and your existing `oauth2-proxy-secrets` Secret. Only `values.yaml` changes. **Remove** the old wiring, noting the values of your existing oauth2-proxy flags for the next step: ```yaml theme={null} thorasDashboard: service: targetPort: 4180 # remove extraContainers: # remove the entire oauth2-proxy container - name: oauth2-proxy # ... ``` **Add** the built-in sidecar configuration, substituting the values you noted: ```yaml theme={null} thorasDashboard: auth: mode: oidc oidc: provider: oidc # or entra-id; reuse your existing --provider value (Okta uses oidc) issuerURL: redirectURL: emailDomains: [yourcompany.com] # your --email-domain value(s) existingSecret: secretName: oauth2-proxy-secrets ``` The chart reads `client-id`, `client-secret`, and `cookie-secret` from the existing Secret by default (key names are configurable under `auth.oidc.existingSecret`). The Service goes back to targeting the dashboard's `containerPort`, now owned by the chart's sidecar, so no `service.targetPort` override is needed. ## Bring your own auth Set `thorasDashboard.auth.enabled: false` if you already gate the dashboard somewhere else, such as an auth plugin at the ingress or gateway, a service-mesh policy, or a custom SSO sidecar wired through `thorasDashboard.extraContainers` and `thorasDashboard.service.targetPort`. ```yaml theme={null} thorasDashboard: auth: enabled: false ``` With built-in auth disabled, the dashboard's nginx listens on `thorasDashboard.containerPort` with nothing in front of it. Any workload in the cluster that can reach the `thoras-dashboard` Service can then use part of the Thoras API without authentication, bypassing the shared `apiClientSecret`. Only disable built-in auth if something external gates every request. The [audit trail](/guides/audit-trail) also can't attribute actions to a person in this mode; every action is recorded as `unknown`. ## NetworkPolicy and OIDC If you enable the chart's [NetworkPolicy](/installation/advanced-setup#networkpolicy) alongside OIDC mode, the chart opens egress on port `443` from the dashboard pod so the sidecar can reach your identity provider. Tighten this with a layered policy scoped to your IdP's hostnames (for example a `CiliumNetworkPolicy` with `toFQDNs`) if your security posture requires it. ## Further reading * [oauth2-proxy documentation home](https://oauth2-proxy.github.io/oauth2-proxy/) * [Configuration overview / full flag reference](https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview) * [Session storage options](https://oauth2-proxy.github.io/oauth2-proxy/configuration/session_storage) * [TLS configuration](https://oauth2-proxy.github.io/oauth2-proxy/configuration/tls) # Azure Entra OIDC dashboard auth Source: https://docs.thoras.ai/guides/dashboard-auth/entra Log into the Thoras dashboard via Azure Entra ID (formerly Azure AD) using the chart's built-in oauth2-proxy sidecar in OIDC mode. This guide covers the Entra-specific steps for authenticating dashboard users against Azure Entra ID. See the [dashboard authentication overview](/guides/dashboard-auth) for how the built-in sidecar works and how the two auth modes compare. If you already run your own oauth2-proxy sidecar against Azure Entra ID from a pre-5.0 install, your app registration and Secret carry over unchanged. See [Migrating from a hand-rolled oauth2-proxy sidecar](/guides/dashboard-auth#migrating-from-a-hand-rolled-oauth2-proxy-sidecar). ## Setup ### 1. Register an app in Azure Entra 1. In the Azure Portal, go to **Microsoft Entra ID -> App registrations -> New registration**. 2. For **Supported account types**, pick **Accounts in this organizational directory only (single tenant)**. Multi-tenant works but needs extra configuration (see the `issuerURL` field note below). 3. Set a **Redirect URI** of type "Web": `https://thoras.yourcompany.com/oauth2/callback`. 4. Note the **Application (client) ID** and **Directory (tenant) ID**. You'll need both. 5. Under **Certificates & secrets**, create a new client secret and copy its value immediately (it's only shown once). 6. **API permissions** need no changes. The default `Microsoft Graph User.Read` permission is enough for sign-in; oauth2-proxy requests the `openid email profile` scopes at sign-in, and users consent to them on first login. azure-app-registration See oauth2-proxy's own walkthrough of this same registration process: [Microsoft Entra ID provider config](https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/ms_entra_id). ### 2. Create the Kubernetes secret The sidecar needs three values, read from a Kubernetes Secret you manage rather than inlined in `values.yaml`. The chart never generates these: | Key | Purpose | | - | - | | `client-id` | The application's client ID | | `client-secret` | The application's client secret | | `cookie-secret` | A random value oauth2-proxy uses to encrypt its session cookie | Generate the cookie secret with: ```bash theme={null} openssl rand -base64 32 | head -c 32 | base64 ``` Create the secret (values below are base64-encoded automatically by `kubectl` when using `--from-literal`): ```bash theme={null} kubectl create secret generic oauth2-proxy-secrets \ --namespace thoras \ --from-literal=client-id="" \ --from-literal=client-secret="" \ --from-literal=cookie-secret="$(openssl rand -base64 32 | head -c 32 | base64)" ``` ### 3. Switch the chart into OIDC mode In `values.yaml`, set `thorasDashboard.auth.mode: oidc` and fill in the OIDC block: ```yaml theme={null} thorasDashboard: auth: mode: oidc oidc: provider: entra-id issuerURL: https://login.microsoftonline.com//v2.0 redirectURL: https://thoras.yourcompany.com/oauth2/callback emailDomains: [yourcompany.com] existingSecret: secretName: oauth2-proxy-secrets ``` #### Field notes * `provider`: `entra-id` selects Entra-specific handling in oauth2-proxy (group overage via Microsoft Graph, multi-tenant issuer checks). `oidc` also works against Entra but drops those Entra-specific features. * `issuerURL`: points at your Entra tenant's v2.0 OIDC endpoint. Replace `` with your Directory (tenant) ID. For a multi-tenant app, use `https://login.microsoftonline.com/common/v2.0` and add `--insecure-oidc-skip-issuer-verification` to `thorasDashboard.auth.extraArgs` (oauth2-proxy will otherwise reject the per-tenant issuer returned in tokens). See the [Entra ID provider docs](https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/ms_entra_id) for the full multi-tenant setup, including `entra-id-allowed-tenant`. * `redirectURL`: must be the real external hostname of the dashboard, over HTTPS, and must exactly match a redirect URI registered on the Entra app registration. * `emailDomains`: restricts login to your org's email domain(s). Use `["*"]` to allow any authenticated user. * `existingSecret.secretName`: points at the Secret you created in step 2. ### 4. Deploy and verify 1. `helm upgrade` the release with the updated `values.yaml`. 2. Confirm the dashboard pod comes up healthy, and check the sidecar logs if something goes wrong: ```bash theme={null} kubectl -n thoras get pods -l app=thoras-dashboard kubectl -n thoras logs deploy/thoras-dashboard -c oauth2-proxy ``` 3. Visit the dashboard URL. You should be redirected to your identity provider's login page, and land back on the dashboard after authenticating. 4. If the redirect fails or loops, double-check that `redirectURL` exactly matches a redirect URI registered on your identity provider's app, and that `issuerURL` is correct. ## Exposing the sidecar externally The chart's built-in oauth2-proxy sidecar owns the dashboard's `containerPort`, so the existing `thorasDashboard` Service already routes external traffic through the auth check. No `service.targetPort` override is needed. Expose the Service using whichever mechanism the chart already supports: **Ingress** ```yaml theme={null} thorasDashboard: ingress: enabled: true hosts: - host: thoras.yourcompany.com paths: - path: / pathType: Prefix ``` **Gateway API** ```yaml theme={null} thorasDashboard: gatewayAPI: enabled: true parentRefs: - name: eg sectionName: http hostnames: - thoras.yourcompany.com ``` Adjust `ingressClassName`/`parentRefs` to match your cluster's actual Ingress controller or `Gateway`, and terminate TLS there (or upstream at a cert-manager-issued `Certificate` bound to it). Once DNS for `thoras.yourcompany.com` resolves and a valid TLS cert is issued, visiting the dashboard URL should trigger the login flow described above. ## Further reading * [oauth2-proxy documentation home](https://oauth2-proxy.github.io/oauth2-proxy/) * [Microsoft Entra ID provider configuration](https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/ms_entra_id) * [Configuration overview / full flag reference](https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview) * [Session storage options](https://oauth2-proxy.github.io/oauth2-proxy/configuration/session_storage) * [TLS configuration](https://oauth2-proxy.github.io/oauth2-proxy/configuration/tls) # Default (htpasswd) dashboard auth Source: https://docs.thoras.ai/guides/dashboard-auth/htpasswd Log into the Thoras dashboard with the generated username and password, or provide your own. ## Overview `htpasswd` is the default authentication mode. On first install, the Thoras config-controller generates a password and session cookie secret and stores them in the `thoras-config-controller` Secret. The username is `thoras`. You can set your own values, pin them for GitOps, or point the chart at a Secret you manage yourself. When you're ready to integrate with an identity provider, see the [dashboard authentication overview](/guides/dashboard-auth) for OIDC options. ## Logging in The default username is `thoras` (`thorasDashboard.auth.htpasswd.username`). Fetch the generated password with: ```bash theme={null} kubectl get secret thoras-config-controller -n thoras \ -o jsonpath='{.data.dashboard-auth-password}' | base64 -d ``` The Secret appears once config-controller has finished its first reconcile, usually within a minute of install. Until then the dashboard pod reports `CreateContainerConfigError`. This clears on its own. ## Setting the password explicitly Set the values in `values.yaml` to pin them. The chart stores pinned values in the `thoras-helm-values` Secret. That Secret is rendered without random generation or Helm `lookup`, so it is safe for GitOps tools such as Argo CD and Flux. ```yaml theme={null} thorasDashboard: auth: htpasswd: username: thoras password: cookieSecret: <16, 24, or 32 byte secret> # e.g. openssl rand -base64 32 ``` ## Using an existing Secret Point the dashboard at a Secret you manage yourself, for example one provisioned by Sealed Secrets, External Secrets, or SOPS. ```yaml theme={null} thorasDashboard: auth: htpasswd: existingSecret: secretName: my-dashboard-auth passwordKey: dashboard-auth-password cookieSecretKey: dashboard-auth-cookie-secret ``` `existingSecret` takes precedence over `password` and `cookieSecret`. ## Rotating credentials The generated credentials are not intended to be rotated in place. If you need control over rotation, take ownership of the values by pinning them in `values.yaml` or by pointing the chart at an `existingSecret` you manage. The config-controller watches both and restarts the dashboard automatically when the value changes, so there is no manual `kubectl rollout restart` step: * **Pinned in values:** change the value in `values.yaml` and `helm upgrade`. * **Existing Secret:** update your Secret. The change is picked up within about a minute. Rotating the cookie secret logs out every active dashboard session. ## Switching to OIDC To hand authentication off to your identity provider, see the [dashboard authentication overview](/guides/dashboard-auth) and pick the guide for your IdP. # Okta OIDC dashboard auth Source: https://docs.thoras.ai/guides/dashboard-auth/okta Log into the Thoras dashboard via Okta using the chart's built-in oauth2-proxy sidecar in OIDC mode. This guide covers the Okta-specific steps for authenticating dashboard users against Okta. See the [dashboard authentication overview](/guides/dashboard-auth) for how the built-in sidecar works and how the two auth modes compare. If you already run your own oauth2-proxy sidecar against Okta from a pre-5.0 install, your app registration and Secret carry over unchanged. See [Migrating from a hand-rolled oauth2-proxy sidecar](/guides/dashboard-auth#migrating-from-a-hand-rolled-oauth2-proxy-sidecar). ## Setup ### 1. Register an app in Okta 1. In the Okta Admin Console, go to **Applications -> Applications -> Create App Integration**. 2. Choose **OIDC - OpenID Connect** as the sign-in method and **Web Application** as the application type. 3. Set a **Sign-in redirect URI** of `https://thoras.yourcompany.com/oauth2/callback`. 4. Under **Assignments**, grant access to the group(s) that should be able to reach the dashboard (or "Everyone" for org-wide access). 5. Once created, note the **Client ID** and **Client secret** from the app's **General** tab, and your **Okta domain** (e.g. `dev-133337.okta.com`) from the top of the Admin Console. Your issuer URL is `https:///oauth2/default` if you're using the default custom authorization server, `https:///oauth2/` if you've configured a custom authorization server under **Security -> API**, or `https://` if you're using the org authorization server. **The `default` custom authorization server may not be available or usable.** Custom authorization servers (including the one named `default`) are part of Okta's [API Access Management](https://developer.okta.com/docs/concepts/api-access-management/) add-on. Production orgs that don't have that add-on cannot use `/oauth2/default` at all, and newly provisioned Integrator Free Plan orgs ship without an Access Policy on it. Without a matching Access Policy, login fails with `You are not allowed to access this app` and a `no_matching_policy` error in the Okta system log. Fix it one of two ways: * **Add an Access Policy** to the default custom auth server under **Security -> API -> Authorization Servers -> default -> Access Policies**, then add a rule that applies to your app. * **Use the org authorization server instead** by dropping `/oauth2/default` from the issuer URL. Set the issuer to `https://`. This works on every Okta org and needs no add-on. See [Authorization servers (Okta docs)](https://developer.okta.com/docs/concepts/auth-servers/). See oauth2-proxy's own Okta walkthrough (under the OpenID Connect provider, which is what Okta uses): [OpenID Connect - Okta](https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/openid_connect#okta), and Okta's writeup of the same integration: [Add Auth to Any App with OAuth2 Proxy](https://developer.okta.com/blog/2022/07/14/add-auth-to-any-app-with-oauth2-proxy). ### 2. Create the Kubernetes secret The sidecar needs three values, read from a Kubernetes Secret you manage rather than inlined in `values.yaml`. The chart never generates these: | Key | Purpose | | - | - | | `client-id` | The application's client ID | | `client-secret` | The application's client secret | | `cookie-secret` | A random value oauth2-proxy uses to encrypt its session cookie | Generate the cookie secret with: ```bash theme={null} openssl rand -base64 32 | head -c 32 | base64 ``` Create the secret (values below are base64-encoded automatically by `kubectl` when using `--from-literal`): ```bash theme={null} kubectl create secret generic oauth2-proxy-secrets \ --namespace thoras \ --from-literal=client-id="" \ --from-literal=client-secret="" \ --from-literal=cookie-secret="$(openssl rand -base64 32 | head -c 32 | base64)" ``` ### 3. Switch the chart into OIDC mode In `values.yaml`, set `thorasDashboard.auth.mode: oidc` and fill in the OIDC block: ```yaml theme={null} thorasDashboard: auth: mode: oidc oidc: provider: oidc issuerURL: https:///oauth2/default redirectURL: https://thoras.yourcompany.com/oauth2/callback emailDomains: [yourcompany.com] existingSecret: secretName: oauth2-proxy-secrets ``` #### Field notes * `provider`: `oidc`. oauth2-proxy has no Okta-specific provider; Okta is served by the generic OpenID Connect provider, which is what oauth2-proxy's own Okta example uses. `okta` is not a valid value and the sidecar will fail to start. * `issuerURL`: points at your Okta authorization server. Use `https:///oauth2/default` for the default custom authorization server, `https:///oauth2/` for another custom one, or `https://` for the org authorization server (see the Warning above). * `redirectURL`: must be the real external hostname of the dashboard, over HTTPS, and must exactly match the sign-in redirect URI registered on the Okta app. * `emailDomains`: restricts login to your org's email domain(s). Use `["*"]` to allow any authenticated user. * `existingSecret.secretName`: points at the Secret you created in step 2. ### 4. Deploy and verify 1. `helm upgrade` the release with the updated `values.yaml`. 2. Confirm the dashboard pod comes up healthy, and check the sidecar logs if something goes wrong: ```bash theme={null} kubectl -n thoras get pods -l app=thoras-dashboard kubectl -n thoras logs deploy/thoras-dashboard -c oauth2-proxy ``` 3. Visit the dashboard URL. You should be redirected to your identity provider's login page, and land back on the dashboard after authenticating. 4. If the redirect fails or loops, double-check that `redirectURL` exactly matches a redirect URI registered on your identity provider's app, and that `issuerURL` is correct. ## Exposing the sidecar externally The chart's built-in oauth2-proxy sidecar owns the dashboard's `containerPort`, so the existing `thorasDashboard` Service already routes external traffic through the auth check. No `service.targetPort` override is needed. Expose the Service using whichever mechanism the chart already supports: **Ingress** ```yaml theme={null} thorasDashboard: ingress: enabled: true hosts: - host: thoras.yourcompany.com paths: - path: / pathType: Prefix ``` **Gateway API** ```yaml theme={null} thorasDashboard: gatewayAPI: enabled: true parentRefs: - name: eg sectionName: http hostnames: - thoras.yourcompany.com ``` Adjust `ingressClassName`/`parentRefs` to match your cluster's actual Ingress controller or `Gateway`, and terminate TLS there (or upstream at a cert-manager-issued `Certificate` bound to it). Once DNS for `thoras.yourcompany.com` resolves and a valid TLS cert is issued, visiting the dashboard URL should trigger the login flow described above. ## Further reading * [oauth2-proxy documentation home](https://oauth2-proxy.github.io/oauth2-proxy/) * [oauth2-proxy OpenID Connect provider - Okta section](https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/openid_connect#okta) * [Add Auth to Any App with OAuth2 Proxy (Okta blog)](https://developer.okta.com/blog/2022/07/14/add-auth-to-any-app-with-oauth2-proxy) * [Okta authorization servers (org vs. custom)](https://developer.okta.com/docs/concepts/auth-servers/) * [Configuration overview / full flag reference](https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview) * [Session storage options](https://oauth2-proxy.github.io/oauth2-proxy/configuration/session_storage) * [TLS configuration](https://oauth2-proxy.github.io/oauth2-proxy/configuration/tls) # Diagnostics Export Source: https://docs.thoras.ai/guides/diagnostics Download a diagnostics package for troubleshooting and support A diagnostics package is a zip file containing operational data about your Thoras deployment. Since Thoras runs self-hosted in your cluster, this is the primary way to share deployment context with the Thoras team when you need support or want a configuration review. It does not include application secrets, credentials, environment variables, workload application logs, or business data. You can always unzip and inspect it before sharing. ## What's included * Cluster info (node types, capacity, availability zones) * Thoras configuration and workload scaling settings * Scaling history (suggestions, metrics, resource usage over time) * Cost and utilization data * Optionally, Thoras service logs ## Downloading from the dashboard ### Cluster-wide export From the dashboard homepage, open the **Manage Cluster** dropdown in the header and select **Download diagnostics**. This exports data for all enrolled AIScaleTargets. The dashboard uses a 7-day time range and includes Thoras pod logs by default (you can uncheck this). Full per-workload metrics are excluded from cluster-wide exports to keep file size manageable. ### Single-AST export Navigate to a specific AIScaleTarget page, then open **Manage Cluster** and select **Download diagnostics**. This exports data for that workload only, with full metrics included. To include all ASTs from this page instead, check **Include all AI Scale Targets** in the export dialog. ## Downloading via API Port-forward the API server: ```bash theme={null} kubectl port-forward svc/thoras-api-server-v2 8080:80 -n thoras ``` Then download: ```bash theme={null} curl -o diagnostics.zip \ "http://localhost:8080/v1/export?start=2026-10-01T00%3A00%3A00Z&end=2026-10-14T00%3A00%3A00Z&include-pod-logs=true&include-metrics=false" ``` ### Query parameters | Parameter | Type | Required | Default | Description | | - | - | - | - | - | | `start` | ISO 8601 timestamp | Yes | -- | Start of the time range | | `end` | ISO 8601 timestamp | Yes | -- | End of the time range | | `include-pod-logs` | boolean | No | `false` | Include Thoras service pod logs | | `include-metrics` | boolean | No | `false` | Include full metrics for the time range. When false, only the last 10 minutes of metrics are included per AST. | | `namespace` | string | No | all | Filter to a specific namespace | | `targetName` | string | No | all | Filter to a specific AIScaleTarget name (requires `namespace`) | ### Examples **Cluster-wide with pod logs:** ```bash theme={null} curl -o cluster-diagnostics.zip \ "http://localhost:8080/v1/export?start=2026-10-01T00%3A00%3A00Z&end=2026-10-14T00%3A00%3A00Z&include-pod-logs=true&include-metrics=false" ``` **Single workload with full metrics:** ```bash theme={null} curl -o workload-diagnostics.zip \ "http://localhost:8080/v1/export?start=2026-10-01T00%3A00%3A00Z&end=2026-10-14T00%3A00%3A00Z&include-metrics=true&namespace=my-namespace&targetName=my-workload" ``` **All workloads in a namespace:** ```bash theme={null} curl -o namespace-diagnostics.zip \ "http://localhost:8080/v1/export?start=2026-10-01T00%3A00%3A00Z&end=2026-10-14T00%3A00%3A00Z&namespace=my-namespace" ``` Full metrics can be large for clusters with many ASTs. For cluster-wide exports, consider leaving `include-metrics=false` and only requesting full metrics for specific workloads you're investigating. ## Sharing with Thoras Send your diagnostics package to the Thoras team via your shared Slack channel, email at [support@thoras.ai](mailto:support@thoras.ai), or any file transfer tool your team prefers (e.g., [magic-wormhole](https://github.com/magic-wormhole/magic-wormhole)). # Enabling Autonomous Mode Source: https://docs.thoras.ai/guides/enabling-autonomous-mode Enable predictive scaling with confidence Following this quick checklist helps ensure your workloads are ready for predictive autoscaling. ## Autonomous Mode Checklist 1. For autonomously scaling, Thoras requires the ability to persist historical metrics, which requires a Kubernetes persistent volume. See [Persistent Storage](../installation/persistent-storage) for a walkthrough 2. Forecast worker capacity scales automatically as you enroll more workloads. If you need to pin a fixed pool size, see [Forecast worker capacity](../installation/advanced-setup#forecast-worker-capacity) 3. Deploying Thoras with ArgoCD? Check out the [ArgoCD config considerations](../guides/argo-cd) 4. \[Optional] `kubectl` port-forwarding is a great way to experiment, but you may eventually want to expose the dashboard service internally to enable broader, more stable access. You can do this by enabling `Ingress` via `$.Values.thorasDashboard.ingress` or `GatewayAPI` via `$.Values.thorasDashboard.gatewayAPI`. See [GitHub](https://github.com/thoras-ai/helm-charts/blob/main/charts/thoras/README.md#thoras-dashboard) for further documentation 5. **Check predictions in the dashboard** - confirm metric suggestions for your workload are accurate. The Thoras reasoning engine typically needs to see a pattern twice to learn about it; so model performance can take between a few minutes to a few days to reach maximum accuracy. predictions-vs-usage 6. **Wait for sufficient historical data** - Autonomous scaling requires at least 24 hours of historical data by default before it can be enabled. This ensures enough data confidence for safe scaling decisions. Configure the default via the Helm value `thorasForecast.minLookbackToScale`, or override it per-AST via `model.min_lookback_to_scale` in the [AST definition](../reference/ast-definition#modelmin_lookback_to_scale). 7. **Understand application behavior** - for vertical scaling, ensure the workload and its dependencies can handle periodic restarts. See the [vertical pod rightsizing](../guides/vertical-pod-rightsizing) and [aiscaletarget](../reference/ast-definition) pages for an overview of scaling and restart configuration options. 8. **Choose a scaling direction** - only one scaling direction (horizontal or vertical) can be in autonomous mode at a time. Choose the direction that best fits your workload: * **Horizontal** for workloads with variable traffic patterns that need to scale the number of replicas to handle demand spikes. * **Vertical** for workloads with more predictable resource patterns that benefit from optimized CPU and memory requests to improve cluster utilization and reduce costs. **Note:** before enabling `Autonomous Mode`, verify no competing vertical autoscaling controllers are modifying pod resource requests for the same workloads. Running multiple controllers that attempt to adjust pod resource allocation can lead to unstable behavior, oscillation, or conflicting decisions. See [Understanding Vertical and Horizontal Scaling Modes](./configuring-asts#understanding-vertical-and-horizontal-scaling-modes) for more details. # Choosing a Forecast Optimization Strategy Source: https://docs.thoras.ai/guides/forecast-optimization-strategies Thoras forecasts future demand for your workload and recommends resource levels accordingly. You can choose an optimization strategy that reflects your operational priorities. All strategies forecast demand and remain peak-aware. The difference is how closely recommendations track usage versus how much additional margin they maintain. Thoras modes span a spectrum from cost optimization to assurance focused: `undershoot` → `cost_optimized` → `balanced` → `assurance_optimized` → `overshoot` Because Thoras is forecasting future demand, no mode eliminates forecast misses entirely. More aggressive modes are more likely to run closer to real demand, while more conservative modes maintain more room above it. `percentile` is an alternative mode that sets the forecast directly from historical usage rather than predicting future demand. ## Quick Reference | Strategy | Approach | Recommended For | | - | - | - | | `undershoot` | Allows recommendations to run below usage more often | Non-critical or resilient jobs | | `cost_optimized` | Tracks tightly with minimal headroom | Async, retry-safe workloads | | `balanced` (default) | Tracks usage with balanced efficiency and assurance | General production workloads | | `assurance_optimized` | Tracks above usage with moderate headroom | User-facing or consistency-sensitive workloads | | `overshoot` | Maintains elevated headroom based on historical usage | Critical workloads requiring additional consistency | | `percentile` | Forecast set at Nth percentile of historical usage | Stable workloads with a specific coverage target | ## Mode Details ### `balanced` (default) * Balances efficiency and assurance with comfortable headroom * Provides strong coverage against historically observed demand volatility * Choose this for production workloads where you want both optimization and consistency ### `cost_optimized` * Uses tighter recommendations to reduce unused capacity * May allow occasional brief resource pressure during rare demand spikes * Best for workloads that tolerate moderate variability: background jobs, async services, retry-safe systems ### `undershoot` * Most aggressive low-headroom strategy * Intentionally allows recommendations to run below observed demand more often * Best for non-critical workloads, batch processing, or systems with strong retry logic and fallback behavior ### `assurance_optimized` * Tracks above typical usage patterns while remaining responsive to changes * Stays close to usage for steady workloads, increasing headroom when variability rises * Best for workloads where consistency is important without significantly increasing resource usage ### `overshoot` * Maintains consistent headroom based on historical usage levels * Less responsive to short-term changes in usage patterns * Best for workloads where maintaining additional headroom is preferred over tighter recommendations ### `percentile` * Forecast is set to the Nth percentile of observed historical usage * `percentile_value` (0–100) is required and determines the coverage level: a value of `80` means 80% of historical data points fell at or below the forecast value * Best for workloads where you want to set a specific historical coverage target rather than rely on demand prediction ```yaml theme={null} spec: model: mode: percentile percentile_value: 80 ``` If not specified, the default is `mode: balanced`. ## Configuring Forecast Mode Set the optimization strategy in your AIScaleTarget: ```yaml theme={null} spec: model: mode: assurance_optimized ``` You can also configure different modes per metric. See [model.metrics reference](/reference/ast-definition#model-metrics) for details. ## Fine-Tuning Forecast Behavior After choosing a forecast optimization strategy, you can further tune forecast behavior using additional settings. These are best used for workload-specific adjustments after selecting your primary mode. ### `forecast_buffer_percentage` Adds a fixed percentage on top of the forecasted recommendation. **Use this when:** * The forecast is generally reasonable, but you want it to run slightly higher or lower overall **Reference:** [model.forecast\_buffer\_percentage](/reference/ast-definition#model-forecast-buffer-percentage) ### `forecast_interval` Controls how often Thoras generates a new forecast. **Use this when:** * Workload behavior changes quickly and recommendations need to refresh more often **Reference:** [model.forecast\_interval](/reference/ast-definition#model-forecast-interval) ### `forecast_blocks` Controls how far into the future each forecast covers. Thoras sizes for the predicted **peak** inside that window, so a longer window holds capacity for any peak that falls within it. **Use this when:** * You want Thoras to account for demand farther ahead in time **Note:** `forecast_blocks` should be greater than or equal to `forecast_interval` to ensure each forecast covers the next update window. **Reference:** [model.forecast\_blocks](/reference/ast-definition#model-forecast-blocks) ### Quick Guidance If you're deciding what to adjust: * Change **`mode`** when you want forecasts to behave more aggressively or conservatively * Change **`forecast_buffer_percentage`** when the forecast shape looks right, but you want to raise or lower the average prediction * Change **`forecast_interval`** when you want forecasts to update more or less frequently * Change **`forecast_blocks`** when you want to adjust how far ahead forecasts look (longer horizons are more conservative but may increase overprovisioning) **In most cases, `model.mode` should be your first tuning lever.** The settings above are for smaller workload-specific adjustments. Your model mode also determines default scaling thresholds, controlling how large a suggested change must be before a scaling action fires. See [`scaling_behavior`](/reference/ast-definition#scaling_behavior) for details. # Predictive Horizontal Pod Autoscaling Source: https://docs.thoras.ai/guides/hpa This guide explains how to set up Predictive Horizontal Pod Autoscaling in Kubernetes using Thoras, including the difference between recommendation and autonomous modes, and how to scale using external or custom metrics. ## What is Traditional Horizontal Pod Autoscaling? Horizontal Pod Autoscaling (HPA) is a Kubernetes feature that adjusts the number of pods in a deployment, statefulset, or other scalable resource based on specified metrics, which can include built-in metrics (CPU, memory) or external/custom metrics (e.g. queue length, latency). HPA increases or decreases the number of pods ***after*** metrics cross pre-defined thresholds **Limitations of Traditional HPA:** While traditional HPA is powerful, it only reacts to spikes in usage after they happen. This means your application may scale up too late to handle sudden surges in traffic, forcing you to keep extra capacity running "just in case", which can be costly and inefficient. Another common shortcoming with traditional HPA is that it is prone to rubber-banding if not carefully managed. For example, a scale down quickly followed by a scale back up, which can negatively impact both reliability and cost through pod churn. **How Thoras Improves Autoscaling:** Thoras brings Predictive Horizontal Pod Autoscaling to Kubernetes. Instead of waiting for a spike to occur, Thoras analyzes trends and forecasts future demand, scaling your pods ***before*** the spike even begins. This proactive approach gives workloads greater resilience and allows running at much higher utilization targets, eliminating the need to maintain a large buffer of spare capacity "just in case". Ultimately, this can significantly reduce cloud waste while maximizing resilience to increases in usage. In this guide, we will walk you through how to set up Predictive Horizontal Pod Autoscaling with Thoras so your pods can scale ahead of demand spikes, giving you both efficiency and peace of mind. ## Thoras Scaling Modes: Recommendation vs. Autonomous Thoras supports two modes for managing scaling: ### Recommendation Mode * Thoras analyzes your workload usage, makes accurate predictions of your workload's *future usage*, and suggests the optimal number of pods based on those predictions. * You (or your automation) can review and apply these recommendations. ### Autonomous Mode * Thoras actively manages horizontal scaling for your workload by providing your HPA the ability to scale on future usage in addition to real-time usage. The number of desired pods for any given scaling decision will always be the higher number of suggested pods between real-time (provided by HPA) and future-time (provided by Thoras). ## How Thoras and HPA coordinate Thoras and HPA run side by side and each contribute to the final replica count. Thoras never replaces or modifies your HPA. HPA is reactive: it responds to what is happening right now, scaling when metrics cross a threshold. Thoras is predictive: it scales based on where your workload is headed, ahead of demand. Together, HPA handles truly unexpected spikes while Thoras handles predictable load patterns. Neither operates in isolation, and neither replaces the other. ## Setting Up Predictive HPA with Thoras ### Prerequisites * A running Kubernetes cluster with [Kubernetes metrics server](https://github.com/kubernetes-sigs/metrics-server) installed * Thoras installed and running (see [quickstart guide](../getting-started/quickstart)) ### 1. Create an HPA (if not already present) A single existing HPA is required for each target deployment, which is turbo-charged with Thoras. Check whether an HPA exists ```sh theme={null} kubectl get hpa -n ``` For Thoras to work autonomously, you must have an HPA defined for that workload. However, if you just want to view recommendations, Thoras can do so without an HPA by assuming you want suggestions that would scale you to a target of 80% utilization. Example defining HPA for `AIScaleTarget`: ```sh theme={null} kubectl autoscale deployment -n --cpu-percent=80 --min=1 --max=10 ``` ### 2. Register an `AIScaleTarget` See [Configuring AIScaleTargets](./configuring-asts) for detailed steps on creating and applying an `AIScaleTarget` manifest. ### 3. Setting Horizontal Mode \[ recommendation | autonomous ] #### Recommendation Mode `AIScaleTarget`s should start in `recommendation` mode while the models learn usage and scaling patterns. To enable `recommendation` mode simply set the `AIScaleTarget`'s `spec.horizontal.mode` to `recommendation`. For example, in my-ast.yaml: ```yaml theme={null} --- spec: horizontal: mode: recommendation ``` Apply the update ```sh theme={null} kubectl apply -f my-ast.yaml ``` #### Autonomous Mode Workloads can be switched to `autonomous` with as little as 24 hours of data (the default, controlled by `thorasForecast.minLookbackToScale` in the Helm chart, or overridden per-AST via `model.min_lookback_to_scale`). However, if your workload has week-day seasonality, we recommend running in `recommendation` mode for as much as a week to ensure the models can capture the seasonality. After models are sufficiently trained in `recommendation` mode, the mode may be switched to `autonomous` where scaling recommendations are automatically applied. To enable `autonomous` mode simply set the `AIScaleTarget`'s `spec.horizontal.mode` to `autonomous`. For example, in my-ast.yaml: ```yaml theme={null} --- spec: horizontal: mode: autonomous ``` Apply the update ```sh theme={null} kubectl apply -f my-ast.yaml ``` > **Note:** Only one scaling direction (horizontal or vertical) can be in > `autonomous` mode at a time. See > [Configuring AIScaleTargets](./configuring-asts#understanding-vertical-and-horizontal-scaling-modes) > for additional details. ## Constraining Replicas to Fixed Multiples Some workloads require specific replica counts for even load distribution across shards or partitions. A Kafka consumer group with 8 partitions, for example, distributes load unevenly at any replica count that is not a multiple of 8. A StatefulSet sharded across a fixed topology, where each pod owns a specific shard, has the same constraint. Use `multipleOf` on `spec.horizontal` to constrain Thoras to only scale to valid replica counts: ```yaml theme={null} --- spec: horizontal: mode: autonomous maxReplicas: 24 multipleOf: 8 ``` With this configuration, Thoras will only ever set replica counts of 8, 16, or 24. **Note:** if `maxReplicas` is not itself a multiple of `multipleOf`, the effective scaling ceiling is the largest multiple that fits within `maxReplicas`. For example, `maxReplicas: 20` with `multipleOf: 8` caps effective scaling at 16, not 20. Set `maxReplicas` to a multiple of `multipleOf` to avoid an unreachable ceiling. See [`horizontal.multipleOf`](/reference/ast-definition#horizontal-multipleof) in the reference for full configuration details. ## Constraining Replicas to a Fixed Set `multipleOf` (above) fits workloads whose valid counts are regular multiples of a single value. Other workloads have an irregular set of valid counts. A topic with 240 partitions, for example, distributes evenly at any count that divides 240, such as 4, 6, 8, 12, and 16. For cases like this, use `allowedReplicas` on `spec.horizontal` to list the exact set Thoras may choose from: ```yaml theme={null} --- spec: horizontal: mode: autonomous minReplicas: 4 maxReplicas: 20 allowedReplicas: [4, 6, 8, 12, 16] ``` Thoras rounds any desired count up to the smallest value in the set that meets or exceeds it, and never scales beyond the largest value. `allowedReplicas` cannot be combined with `multipleOf`. See [`horizontal.allowedReplicas`](/reference/ast-definition#horizontal-allowedreplicas) in the reference for full configuration details. ## Using External or Custom Metrics By default, HPA can scale on CPU and memory. Thoras and Kubernetes also support scaling on external or custom metrics (e.g., http request rate, queue writes/depth). Example: Scaling on an External Metric Add an external metric to your `AIScaleTarget`: ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} spec: scaleTargetRef: kind: Deployment name: {{YOUR_AST_NAME}} horizontal: mode: autonomous additional_metrics: - type: External external: metric: name: {{YOUR_CUSTOM_METRIC}} selector: matchLabels: source: prometheus target: type: AverageValue averageValue: 100 ``` * `type: External`: tells HPA to use an external metric. * `external.metric.name`: the name of the metric (must match what your metrics adapter provides). * `external.target.averageValue`: the target value for scaling. ## Verifying HPA and Thoras Integration * Check HPA status: ```sh theme={null} kubectl describe hpa -n ``` * Check `AIScaleTarget` status: ```sh theme={null} kubectl get aiscaletarget -n -o yaml ``` * Monitor pod scaling events: ```sh theme={null} kubectl get pods -w ``` # In-Place Resize Behavior Source: https://docs.thoras.ai/guides/in-place-resize How Thoras handles in-place pod resizing, what happens when a resize cannot complete, and the safety controls that protect your workloads. When vertical scaling is configured with `update_mode: in_place` or `update_mode: in_place_or_recreate`, Thoras patches running pods directly through the Kubernetes resize API instead of recreating them. This page explains what happens when that resize cannot complete and how Thoras responds depending on your update policy. For configuration details, see [Update Policy](/guides/vertical-pod-rightsizing#update-policy) and the [AIScaleTarget reference](/reference/ast-definition#update-policy). ## When a Resize Cannot Complete After Thoras patches a pod's resource requests, the kubelet attempts to actuate the change by adjusting the container's cgroup. Two situations can prevent this: * **Insufficient node capacity.** An upsize requires more CPU or memory than the node has available. The kubelet cannot expand the cgroup and the resize stalls. * **Memory limit below live usage.** A memory limit decrease that would drop below the container's current working set is accepted by the API server (the patch succeeds), but the kubelet cannot safely shrink the cgroup. It retries on every sync cycle. In both cases, Kubernetes reports the stall through a `PodResizeInProgress` condition on the pod with `Reason: Error`. The pod continues running with its previous resource allocation. No data is lost and no containers are restarted by the stall itself. ## Behavior by Update Mode How Thoras responds to a stalled resize depends on `update_mode`: ### `in_place` Thoras never evicts or recreates the pod. If the kubelet cannot complete the resize, the pod stays running with its current resources. The stall resolves on its own when conditions change (for example, other pods are descheduled and free up node capacity, or a new forecast lowers the recommendation). This mode is appropriate for workloads where continuity is more important than converging to the recommendation quickly. ### `in_place_or_recreate` Thoras attempts in-place resize first. If a pod's resize stalls for more than two minutes, Thoras evicts that specific pod so it can be recreated with the correct resources by its controller (Deployment, StatefulSet, etc.). Only the stalled pods are evicted; pods that resized successfully are left alone. This is the recommended mode for most workloads because it combines the low-disruption benefits of in-place resize with a reliable fallback. ## Safety Controls Thoras applies several layers of protection when evicting pods as part of the recreate fallback. ### PodDisruptionBudgets Evictions use the [Kubernetes Eviction API](https://kubernetes.io/docs/concepts/scheduling-eviction/api-eviction/), which enforces your PodDisruptionBudgets. If a PDB would be violated, the API server rejects the eviction and Thoras defers the remaining pods to a later run. This is different from a rollout restart, which replaces the entire ReplicaSet and is not gated by PDBs at the individual pod level. ### Disruption batching Evictions are paced using the [`disruption`](/reference/ast-definition#disruption) settings on the AIScaleTarget. Thoras applies changes in batches (default: up to 20% of replicas at a time, with at least 60 seconds between batches) and counts pods that are already terminating or not-Ready against the budget. This prevents stacking disruption when a prior batch has not finished rolling. ### Circuit breaker If pods keep coming back stale after eviction for the same recommendation (for example, because the admission webhook is not applying the new values), Thoras stops evicting. The circuit breaker opens after repeated no-progress cycles and backs off exponentially before probing again. This prevents runaway eviction loops. ## Observability The following signals cover the disruption lifecycle discussed on this page: | Signal | Type | What it tells you | | - | - | - | | [`thoras_disruption_circuit_breaker_open`](/guides/metrics#disruption-circuit-breaker) | Prometheus gauge | Breaker is tripped: evictions stopped for this AST | | [`thoras_ast_disruption_completions_total`](/guides/metrics#ast-disruption-completions) | Prometheus counter | A disruption cycle finished (all stale pods converged) | | `DisruptionInProgress` condition | AST status | A batch is actively rolling (`BatchStarted`, `BatchEvicted`, or `Complete`) | | `DisruptionCircuitBreaker` condition | AST status | Breaker state (`Open`, `Closed`, `HalfOpenProbe`) | Prometheus metrics are detailed on the [Monitoring Thoras](/guides/metrics) page. AST status conditions are visible via `kubectl get ast -n -o jsonpath='{.status.conditions}'`. ## Operational Guidance ### Concurrent upsizes on the same node When multiple pods on the same node receive an upsize at the same time, the node may not have enough capacity for all of them. Thoras handles each pod independently: pods that resize successfully stay in place, and pods that stall follow the fallback path for the configured `update_mode`. To reduce the likelihood of node-capacity stalls: * **Reserve headroom on nodes.** If you use a node autoscaler like Karpenter, consider configuring a small amount of reserved capacity so upsizes have room to land without waiting for a new node. * **Use `disruption` settings to pace changes.** Smaller batch sizes and longer intervals give the node autoscaler time to respond between batches. * **Rely on forecasting.** Thoras predicts resource needs ahead of time, so upsizes are typically applied before a spike rather than during one, reducing contention. ### Choosing between `in_place` and `in_place_or_recreate` | Consideration | `in_place` | `in_place_or_recreate` | | - | - | - | | Pod continuity | Guaranteed. Pods are never evicted by Thoras. | Pods may be evicted if resize stalls. | | Convergence speed | Slower. Stalls resolve only when external conditions change. | Faster. Stalled pods are recreated with correct resources. | | Best for | Stateful workloads, long-running jobs, workloads sensitive to restarts. | General-purpose services where converging to the recommendation matters more than avoiding a single pod restart. | # JVM Workload Scaling Source: https://docs.thoras.ai/guides/jvm-scaling How Thoras optimizes Java-based applications Thoras can predictively tune JVM heap settings (`-Xmx` and `-Xms`) alongside container cpu and memory requests for Java workloads. Instead of hard-coding heap sizes or relying on JVM ergonomics, Thoras forecasts heap and non-heap memory usage and derives optimal values on every pod creation. ## How It Works ### Overview Traditional JVM memory configuration is static: you set `-Xmx` (maximum heap) and `-Xms` (initial heap) at deployment time and hope they're sufficient. If heap usage grows, you get OOM kills. If it shrinks, you waste resources. Thoras solves this by: 1. **Collecting JVM metrics** (`jvm_memory_used_bytes` for heap and non-heap areas) directly from your application 2. **Forecasting** future heap and non-heap memory needs using historical patterns 3. **Computing** optimal `-Xmx`, `-Xms`, and container memory request/limit values 4. **Injecting** these as environment variables into pods at creation time via a mutating admission webhook This happens automatically on each pod creation, so your JVM settings stay in sync with actual demand. ### The Calculation Thoras uses three configurable buffers to compute JVM settings from its forecasts: | Parameter | Default | Purpose | | - | - | - | | `xmx_buffer` | 10% | Headroom above observed/predicted heap usage for `-Xmx` | | `xms_ratio` | 80% | Ratio of `-Xms` to `-Xmx` (initial heap as a fraction of max heap) | | `memory_buffer` | 10% | Headroom above total JVM memory (heap + non-heap) for the container memory request | The calculation follows four steps: **Step 1: Determine effective heap and non-heap usage** Thoras takes the **greater** of the forecasted value and current observed value for both heap and non-heap memory. This ensures the JVM is never sized below what it's currently using. ``` effective_heap = max(heap_forecast, heap_current) effective_nonheap = max(nonheap_forecast, nonheap_current) ``` **Step 2: Calculate Xmx (maximum heap)** The maximum heap size is the effective heap usage plus the `xmx_buffer`: ``` Xmx = effective_heap * (1 + xmx_buffer) ``` If `xmx_bounds` are configured, the value is clamped within those bounds. **Step 3: Calculate Xms (initial heap)** The initial heap size is a ratio of the maximum: ``` Xms = Xmx * xms_ratio ``` **Step 4: Calculate container memory request** The container memory request accounts for the full JVM footprint (heap + non-heap) plus a buffer for native memory, thread stacks, metaspace, etc.: ``` container_memory = (Xmx + effective_nonheap) * (1 + memory_buffer) ``` ### Worked Example Given: * Heap forecast: **200 MiB** * Non-heap forecast: **50 MiB** * All buffers at defaults (10% `xmx_buffer`, 80% `xms_ratio`, 10% `memory_buffer`) | Step | Calculation | Result | | - | - | - | | Xmx | 200 MiB \* 1.10 | **220 MiB** | | Xms | 220 MiB \* 0.80 | **176 MiB** | | Container memory | (220 MiB + 50 MiB) \* 1.10 | **297 MiB** | The pod would receive: * `THORAS_JVM_HEAP_MEMORY_XMX=-Xmx230686720` (220 MiB in bytes) * `THORAS_JVM_HEAP_MEMORY_XMS=-Xms184549376` (176 MiB in bytes) * Container memory request: **297 MiB** ### What Gets Injected Thoras injects two environment variables into the JVM container on pod creation: | Environment Variable | Format | Description | | - | - | - | | `THORAS_JVM_HEAP_MEMORY_XMX` | `-Xmx` | Maximum heap size flag (e.g., `-Xmx230686720`) | | `THORAS_JVM_HEAP_MEMORY_XMS` | `-Xms` | Initial/minimum heap size flag (e.g., `-Xms184549376`) | Values are specified in bytes for precision. These variables are prepended to the container's environment so they're available for reference in `JAVA_OPTS` or your application's JVM argument configuration. ## Configuration Guide ### Prerequisites 1. **Prometheus JVM metric endpoint**: Your Java application must expose `jvm_memory_used_bytes` metrics with `area="heap"` and `area="nonheap"` labels. Most JVM metric exporters (Micrometer, JMX Exporter, etc.) produce these by default. 2. **Vertical scaling** enabled on the AIScaleTarget in `auto` (autonomous) mode. ### Step 1: Configure Your Application to Use the Environment Variables Your application must reference the Thoras-injected environment variables for its JVM heap settings. The simplest approach is to reference them in `JAVA_OPTS` or `JAVA_TOOL_OPTIONS`: ```yaml theme={null} containers: - name: my-jvm-app image: my-app:latest env: - name: JAVA_TOOL_OPTIONS value: "$(THORAS_JVM_HEAP_MEMORY_XMX) $(THORAS_JVM_HEAP_MEMORY_XMS)" ``` Or if your entrypoint uses `JAVA_OPTS`: ```yaml theme={null} env: - name: JAVA_OPTS value: "$(THORAS_JVM_HEAP_MEMORY_XMX) $(THORAS_JVM_HEAP_MEMORY_XMS) -XX:+UseG1GC" ``` Kubernetes will resolve the `$(...)` references at container startup using the environment variables that Thoras injects. ### Step 2: Create the AIScaleTarget Configure an AIScaleTarget with JVM options enabled on the target container. The key requirements are: * Vertical mode must be set to `auto` * The container must have `memory.limit.ratio` set (required when JVM auto-sizing is enabled) * `jvm_options.enable_auto_heap_size` must be `true` ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: my-jvm-app namespace: my-namespace spec: scaleTargetRef: kind: Deployment name: my-jvm-app apiVersion: apps/v1 model: mode: balanced vertical: mode: auto containers: - name: my-jvm-app memory: lowerbound: 128Mi limit: ratio: "1.0" jvm_options: enable_auto_heap_size: true ``` ### AIScaleTarget JVM Fields Reference All JVM configuration lives under `spec.vertical.containers[].jvm_options`: | Field | Type | Required | Default | Description | | - | - | - | - | - | | `enable_auto_heap_size` | bool | Yes | `false` | Enables automatic JVM heap sizing for this container. | | `xmx_buffer` | percentage | No | `10%` | Buffer added above observed/predicted heap usage when computing `-Xmx`. Provides headroom to absorb heap growth between forecast cycles. | | `xms_ratio` | percentage | No | `80%` | Ratio of `-Xms` to `-Xmx`. Controls how much heap the JVM pre-allocates at startup. Higher values reduce GC pressure from heap expansion. | | `memory_buffer` | percentage | No | `10%` | Buffer added above total JVM memory (Xmx + non-heap) when computing the container memory request. Accounts for native memory, thread stacks, metaspace growth, etc. | | `xmx_bounds.lowerbound` | resource quantity | No | - | Minimum allowed Xmx value (e.g., `256Mi`). Prevents the heap from being sized too small regardless of forecast. | | `xmx_bounds.upperbound` | resource quantity | No | - | Maximum allowed Xmx value (e.g., `4Gi`). Caps the heap to prevent runaway growth. | The parent container spec also requires: | Field | Relevance | | - | - | | `memory.limit.ratio` | **Required** when `enable_auto_heap_size` is `true`. Defines the ratio of memory limit to request (e.g., `"1.0"` sets limit equal to request). | | `memory.lowerbound` | Minimum container memory request. Acts as a floor. | | `memory.upperbound` | (Optional) Maximum container memory request. Acts as a steady-state ceiling. Bypassed during opt-in [OOM remediation](/guides/oom-remediation). | ### Full Example with Custom Buffers ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: my-jvm-app namespace: my-namespace spec: scaleTargetRef: kind: Deployment name: my-jvm-app apiVersion: apps/v1 model: mode: balanced vertical: mode: auto containers: - name: my-jvm-app memory: lowerbound: 256Mi upperbound: 8Gi limit: ratio: "2.0" jvm_options: enable_auto_heap_size: true xmx_buffer: "15%" xms_ratio: "75%" memory_buffer: "20%" xmx_bounds: lowerbound: 512Mi upperbound: 4Gi ``` This configuration: * Adds 15% headroom on heap forecasts when computing Xmx * Sets Xms to 75% of Xmx * Adds 20% headroom for the container memory request * Clamps Xmx between 512 MiB and 4 GiB regardless of forecast * Keeps the container memory request between 256 MiB and 8 GiB ### Multi-Container Pods JVM auto-scaling supports **one JVM container per pod**. In multi-container pods, only the container with `jvm_options.enable_auto_heap_size: true` will have heap settings managed. Other containers (sidecars, init containers, etc.) are unaffected: ```yaml theme={null} vertical: mode: auto containers: - name: my-jvm-app memory: lowerbound: 256Mi limit: ratio: "2.0" jvm_options: enable_auto_heap_size: true - name: sidecar memory: lowerbound: 64Mi cpu: lowerbound: 50m ``` In this example, `my-jvm-app` gets Thoras-managed heap settings while `sidecar` gets standard vertical scaling (or no scaling, depending on configuration). ## Constraints and Considerations * **One JVM container per pod**: Only one container in the pod can have `enable_auto_heap_size: true`. * **Requires `memory.limit`**: The container must have a `memory.limit.ratio` configured. This ensures the JVM has a bounded memory limit. A ratio of `"2.0"` is typical for JVM workloads since the JVM manages its own memory within the heap boundary. * **Never scales below current usage**: Thoras always takes the maximum of the forecast and current observed usage to prevent sizing below what the JVM is actively using. * **Metrics must be available**: The application must expose `jvm_memory_used_bytes` with `area="heap"` and `area="nonheap"` labels. If these metrics are missing, JVM mutation is skipped for that pod. * **Applied at pod creation**: JVM settings are injected via the admission webhook when pods are created. Existing running pods are not modified in place \-- a rollout is required for new settings to take effect. # Tuning for Large Clusters Source: https://docs.thoras.ai/guides/large-clusters Helm chart settings for clusters with 5,000 or more AIScaleTargets. The chart defaults are sized for small installs. Once a cluster has around 5,000 enrolled `AIScaleTargets` (ASTs), a handful of values need to change to keep Thoras ahead of the load. The configuration below is sized for 5,000 ASTs and was validated on a cluster with over 6,000. Expect Thoras to peak at roughly **100 cores** and **75 GiB of memory**. Steady-state usage is significantly lower. The peak is a short burst that occurs once every training interval (12h by default). Below roughly 5,000 ASTs the defaults are sufficient and none of this is required. Requires chart version `5.5.0` ## Recommended values Apply the following helm chart values. ```yaml theme={null} featureFlags: thorasManageThoras: mode: autonomous metricsCollectorFollowsMode: false thorasWorker: replicas: 2 queriesPerSecond: "100" postgresql: maxConns: 40 maxConnLifetime: 15m thorasApiServerV2: replicas: 3 postgresql: maxConnLifetime: 15m thorasForecast: trainingJitterMinutes: 120 worker: scaler: maxReplicas: 150 metricsCollector: persistence: # Size at roughly 200 MiB per AST; see "Storage" below. 1Ti covers # ~5,000 ASTs. pvcStorageRequestSize: "1Ti" timescale: requests: # TS_TUNE_NUM_CPUS below reads this value, so it must be a whole core # count, not millicores. cpu: "8" memory: 30Gi limits: memory: 30Gi env: - name: TS_TUNE_NUM_CPUS valueFrom: resourceFieldRef: containerName: timescaledb resource: requests.cpu divisor: "1" - name: TS_TUNE_MAX_CONNS value: "500" ``` ## Applying to an existing install The database tunes itself only when its volume is first initialized, so deploying the new values alone will not apply the `TS_TUNE_*` settings to an existing install. Once the new values have rolled out, rerun the tuner and restart the database pod. Replace `thoras` with your release namespace if it differs. ```sh theme={null} kubectl -n thoras exec deploy/metrics-collector -c timescaledb -- \ /docker-entrypoint-initdb.d/001_timescaledb_tune.sh kubectl -n thoras delete pod -l app=metrics-collector ``` The database is unavailable for a short time while the pod restarts. Neither step is needed on a fresh install. ## Thoras components Autonomous mode lets Thoras rightsize its own components as load grows, so you do not have to hand-tune their requests. The database is the exception. `metricsCollectorFollowsMode: false` keeps it in observation mode, and its resources are set explicitly under `metricsCollector.timescale` above. Changing its resources requires a restart, which is disruptive to the database and to the Thoras platform. ## Kubernetes API load Kubernetes API load from the worker is about 2.4 requests per AST per minute (\~200/s at 5,000 ASTs, scaling linearly). Budget for it if you run a fixed-size, self-managed control plane. ## Storage At scale, plan on roughly **200 MiB per AST**, or about 1 TiB for 5,000 ASTs. This is deliberately lower than the 1 GB-per-workload guidance on the [Persistent Storage](/installation/persistent-storage#understanding-usage) page. That rule is sized for small installs, where oversizing is harmless and buys IOPS on cloud block storage. At thousands of ASTs it is excessive (it would call for 5 TiB or more at 5,000 ASTs) and incurs real cost. See [Persistent Storage](/installation/persistent-storage) for storage class, EFS, and existing-volume configuration. ## Questions? For help, email [support@thoras.ai](mailto:support@thoras.ai). # Monitoring Thoras Source: https://docs.thoras.ai/guides/metrics Thoras exposes native Prometheus metrics that offer real-time visibility into the actions and health of the Thoras system. This empowers your teams with deeper insights, transparency, and observability. ## Overview ### Scraping metrics with Prometheus Every Thoras component that emits Prometheus metrics has a `prometheus.enabled` toggle (default `true`) and a fixed `prometheus.port`. Scrape those ports however your Prometheus already discovers targets. If you use the Prometheus Operator, the chart can emit a `ServiceMonitor` for every Thoras component: ```yaml theme={null} serviceMonitor: enabled: true interval: "30s" # optional; empty uses the Prometheus default additionalLabels: # optional; useful for Prometheus discovery release: kube-prometheus-stack ``` ### Action Metrics See Thoras in action. These metrics provide insight into how Thoras actively manages your application resources across your environment. They track when Thoras takes autonomous action to scale horizontally or vertically, when replicas or resource allocations differ from Thoras' desired values, and how each `AIScaleTarget` is currently operating. Together, these metrics give teams the visibility they need to understand scaling behavior, make informed decisions, and maintain efficient, well-performing applications. | Metric Name | Type | Description | Labels | | - | - | - | - | | [`thoras_horizontal_scale_total`](#horizontal-scale-total) | Counter | Counts horizontal scaling actions in autonomous mode | `ai_scale_target`, `scale_metric`, `namespace` | | [`thoras_vertical_scale_total`](#vertical-scale-total) | Counter | Counts vertical scaling adjustments in autonomous mode | `ai_scale_target`, `namespace` | | [`thoras_recommendation`](#recommendation) | Gauge | Current scaling recommendation per `AIScaleTarget` | `ai_scale_target`, `resource`, `namespace`, `container`, `unit` | | [`thoras_scale_targets`](#scale-targets) | Gauge | The count of `AIScaleTargets` in various operational modes (autonomous or recommendation) | `vertical`, `horizontal` | | [`thoras_provisioning_ratio`](#provisioning-ratio) | Gauge | Ratio of current resource usage to the predicted maximum usage within the forecasted period. Values > 1 indicate current usage exceeding predicted maximum, values \< 1 indicate usage below the predicted maximum. | `ai_scale_target`, `namespace`, `scaling_mode`, `resource`, `container`, `mode` | | [`thoras_provisioning_delta`](#provisioning-delta) | Gauge | Absolute difference between current resource usage and predicted maximum usage within the forecasted period. Units depend on resource type (bytes for memory, cores for CPU). | `ai_scale_target`, `namespace`, `scaling_mode`, `resource`, `container`, `mode` | | [`thoras_workload_oom_killed_total`](#workload-oom-killed) | Counter | Total OOM kill events observed per workload | `ast_name`, `namespace` | | [`thoras_ast_active_suggestion`](#ast-active-suggestion) | Gauge | 1 if the `AIScaleTarget` has an active suggestion at last check, 0 if missing. Only set for auto-mode ASTs past their startup grace period. | `ai_scale_target`, `namespace`, `uid` | | [`thoras_ast_metric_integrity`](#ast-metric-integrity) | Gauge | 1 if metrics for the `AIScaleTarget` were collected in the last check interval, 0 if stale. Workloads at zero replicas for the entire window count as healthy. ASTs inside the creation grace period are absent. | `ai_scale_target`, `namespace`, `uid` | | [`thoras_ast_disruption_completions_total`](#ast-disruption-completions) | Counter | Counts completed disruption cycles per `AIScaleTarget`, incremented once every pod is back in sync with the latest recommendation. Counts cycles, not pods, and marks completion rather than the start of the resize. | `ai_scale_target`, `namespace`, `reason` | | [`thoras_disruption_circuit_breaker_open`](#disruption-circuit-breaker) | Gauge | 1 for each `AIScaleTarget` whose eviction circuit breaker is currently tripped. Absence means the breaker is closed. | `ai_scale_target`, `namespace` | ### System Health Metrics Observe the state of Thoras' system health. These metrics offer a window into the health and performance of the Thoras platform. They can also be easily integrated into popular observability tools like Datadog, Grafana, giving your team the flexibility to monitor Thoras wherever you already track system performance. | Metric Name | Type | Description | Labels | | - | - | - | - | | [`thoras_api_http_response_total`](#api-http-response-total) | Counter | Tracks total HTTP responses from internal API; spikes may indicate issues | `path`, `method`, `code` | | [`thoras_api_http_request_duration_seconds`](#api-http-request-duration) | Gauge | Captures average internal API response time; highlights potential slowdowns | `path`, `method` | | [`thoras_forecast_queue_pending_count`](#forecast-queue-pending-count) | Gauge | Number of pending forecast jobs currently in the queue | `action` | | [`thoras_forecast_queue_pending_oldest_duration_seconds`](#forecast-queue-pending-oldest) | Gauge | Duration in seconds that the oldest forecast job has been pending in the queue | | | [`thoras_forecast_queue_status_count`](#forecast-queue-status-count) | Counter | Number of forecast jobs by status | `status` | | [`thoras_job_completions_total`](#job-completions-total) | Counter | Total number of completed jobs by status and kind | `status`, `kind` | | [`thoras_job_duration_seconds`](#job-duration-seconds) | Gauge | Duration of jobs in seconds by status and kind | `status`, `kind` | ## Advanced Usage ### Provisioning Ratio **Metric:** `thoras_provisioning_ratio` The provisioning ratio compares your current resource usage to the Thoras recommended request. The Thoras recommended request is based on the predicted maximum usage during the forecasted period. **Interpretation:** Note: Recommendations are based on the predicted maximum usage over the forecast window. Utilization ratio lowers when preemptive up-scaling occurs. * `ratio > 1.0`: Current usage exceeding predicted maximum * `ratio < 1.0`: Current usage below predicted maximum **Calculation:** * **Vertical scaling**: `current_avg_request / recommended_request` * **Horizontal scaling**: `current_total_usage / forecasted_value` **Example Queries & Dashboards:** ```promql theme={null} # Find workloads with current usage more than 20% above suggested request (under predicted). thoras_provisioning_ratio{scaling_mode="horizontal"} > 1.2 # Count workloads with current usage more than 20% above suggested request (under predicted). count (thoras_provisioning_ratio > 1.2) # Over-provisioned # View provisioning ratio for a specific AIScaleTarget thoras_provisioning_ratio{ai_scale_target="cartservice"} ``` ### Provisioning Delta **Metric:** `thoras_provisioning_delta` The provisioning delta shows the absolute difference between your current resource usage and Thoras recommended request. The Thoras recommended request is based on the predicted maximum usage during the forecasted period. **Units:** * Memory: bytes * CPU: cores **Calculation:** * `abs(current_value - recommended_value)` **Example Queries & Dashboards:** ```promql theme={null} # Find memory deltas exceeding 1GB thoras_provisioning_delta{resource="memory", unit="bytes"} > 1073741824 # View CPU delta for a specific workload thoras_provisioning_delta{ai_scale_target="cartservice", resource="cpu"} # Memory waste from over-provisioning (in GB) - combines ratio + delta (thoras_provisioning_delta{resource="memory", unit="bytes"} * on(ai_scale_target, namespace) group_left() (thoras_provisioning_ratio > 1)) / 1073741824 ``` ### Horizontal Scale Total **Metric:** `thoras_horizontal_scale_total` Tracks the total number of horizontal scaling actions Thoras has performed in autonomous mode. Each increment represents a scaling event where Thoras adjusted replica counts. **Example Queries & Dashboards:** ```promql theme={null} # Total horizontal scaling actions across all targets sum(thoras_horizontal_scale_total) # Scaling actions per AIScaleTarget sum by (ai_scale_target) (thoras_horizontal_scale_total) # Scaling actions per namespace sum by (namespace) (thoras_horizontal_scale_total) # Rate of scaling actions over the last hour rate(thoras_horizontal_scale_total[1h]) ``` ### Vertical Scale Total **Metric:** `thoras_vertical_scale_total` Tracks the total number of vertical scaling actions Thoras has performed in autonomous mode. Each increment represents a scaling event where Thoras adjusted resource requests or limits. **Example Queries & Dashboards:** ```promql theme={null} # Total vertical scaling actions across all targets sum(thoras_vertical_scale_total) # Scaling actions per AIScaleTarget sum by (ai_scale_target) (thoras_vertical_scale_total) # Scaling actions per namespace sum by (namespace) (thoras_vertical_scale_total) # Rate of scaling actions over the last hour rate(thoras_vertical_scale_total[1h]) # Compare horizontal vs vertical scaling activity sum(thoras_horizontal_scale_total) / sum(thoras_vertical_scale_total) ``` ### Recommendation **Metric:** `thoras_recommendation` Shows Thoras' current scaling recommendation for each `AIScaleTarget`. The value and unit depend on the resource type being recommended. **Units:** * Memory: bytes * CPU: cores **Example Queries & Dashboards:** ```promql theme={null} # Current memory recommendations (in GB) thoras_recommendation{resource="memory", unit="bytes"} / 1073741824 # Current CPU recommendations thoras_recommendation{resource="cpu"} # Recommendations for a specific AIScaleTarget thoras_recommendation{ai_scale_target="cartservice"} # Compare recommendations across containers in a workload thoras_recommendation{ai_scale_target="frontend"} by (container, resource) ``` ### Scale Targets **Metric:** `thoras_scale_targets` Reports the count of `AIScaleTargets` currently managed by Thoras, broken down by operational mode (autonomous or recommendation) and scaling dimension (vertical or horizontal). **Example Queries & Dashboards:** ```promql theme={null} # Total AIScaleTargets managed by Thoras sum(thoras_scale_targets) # Count of vertical scaling targets thoras_scale_targets{vertical="true"} # Count of horizontal scaling targets thoras_scale_targets{horizontal="true"} # Targets with both vertical and horizontal scaling enabled thoras_scale_targets{vertical="true", horizontal="true"} ``` ### Workload OOM Killed **Metric:** `thoras_workload_oom_killed_total` Counts the total number of OOM (Out of Memory) kill events observed per workload. Each increment represents a container that was terminated due to exceeding its memory limit. Use this metric to identify workloads that are frequently running out of memory and may need higher memory recommendations. **Example Queries & Dashboards:** ```promql theme={null} # Total OOM kills across all workloads sum(thoras_workload_oom_killed_total) # OOM kills per workload sum by (ast_name) (thoras_workload_oom_killed_total) # OOM kills per namespace sum by (namespace) (thoras_workload_oom_killed_total) # Rate of OOM kills over the last hour rate(thoras_workload_oom_killed_total[1h]) # Workloads with the most OOM kills topk(10, sum by (ast_name, namespace) (thoras_workload_oom_killed_total)) # Alert on OOM kills in the last 15 minutes increase(thoras_workload_oom_killed_total[15m]) > 0 ``` ### AST Active Suggestion **Metric:** `thoras_ast_active_suggestion` Indicates whether each auto-mode `AIScaleTarget` has an active suggestion at the time of the last periodic check. The value is `1` when a suggestion exists and `0` when no suggestion is present. ASTs still within their startup grace period are excluded. **Example Queries & Dashboards:** ```promql theme={null} # ASTs currently missing a suggestion thoras_ast_active_suggestion == 0 # Count of ASTs missing a suggestion count(thoras_ast_active_suggestion == 0) # Alert when any AST has been missing a suggestion for 15m max_over_time(thoras_ast_active_suggestion[15m]) == 0 ``` ### AST Metric Integrity **Metric:** `thoras_ast_metric_integrity` Note: Requires `thorasWorker.enableMetricIntegrityWorker: true` in `values.yaml` (default `true`). Reports whether Thoras has successfully collected CPU/memory metrics for each `AIScaleTarget` in the most recent check interval. The value is `1` when the AST is healthy and `0` when metrics are stale (the workload was running but no CPU/memory samples were collected). Workloads that sat at zero replicas for the entire window count as healthy, since no metrics are expected. ASTs still within their creation grace period, as well as orphaned ASTs whose scale target no longer exists, are absent from the metric entirely. Use this metric to detect a wedged collector or broken metrics pipeline for a specific workload. **Example Queries & Dashboards:** ```promql theme={null} # ASTs with stale metrics thoras_ast_metric_integrity == 0 # Count of ASTs with stale metrics count(thoras_ast_metric_integrity == 0) # Alert when any AST has been stale for 60m max_over_time(thoras_ast_metric_integrity[60m]) == 0 ``` ### AST Disruption Completions **Metric:** `thoras_ast_disruption_completions_total` Counts completed disruption cycles per `AIScaleTarget`. When Thoras applies a new vertical recommendation, it resizes or replaces the workload's pods in batches; once every pod is back in sync with the recommendation, the cycle completes and this counter increments. Use this metric to correlate Thoras resize activity against your own application signals — error rates, latency, or restart counts — without having to infer timing from resource requests changing on the workload. Two things to know about what it counts: * **It counts cycles, not pods.** A cycle that resized four pods increments by one, the same as a cycle that resized one. Cycles resolved entirely by [in-place resize](/guides/vertical-pod-rightsizing) — with no pod restarts at all — are counted too, as are cycles that end early because the update policy changed mid-flight. * **It marks completion, not the start.** Thoras records the cycle as complete on the check that observes every pod back in sync, so the timestamp trails the actual pod changes, typically by up to one disruption interval. When lining this up against an error-rate spike, expect the spike to precede the increment. A series appears for an `AIScaleTarget` only after its first completed cycle, so absence means no cycle has completed yet rather than zero. **Example Queries & Dashboards:** ```promql theme={null} # Disruption cycles completed per workload over the last hour sum by (ai_scale_target, namespace) (increase(thoras_ast_disruption_completions_total[1h])) # Overlay marker: did a cycle complete for this workload in the last 5m? increase(thoras_ast_disruption_completions_total{ai_scale_target="my-workload"}[5m]) > 0 # Busiest workloads by disruption activity topk(10, sum by (ai_scale_target, namespace) (increase(thoras_ast_disruption_completions_total[24h]))) # Cluster-wide disruption rate sum(rate(thoras_ast_disruption_completions_total[30m])) ``` ### API HTTP Response Total **Metric:** `thoras_api_http_response_total` Tracks the total number of HTTP responses from Thoras' internal API. This counter increments for every API response and can help identify unusual traffic patterns or issues. **Example Queries & Dashboards:** ```promql theme={null} # Total API responses sum(thoras_api_http_response_total) # Response rate over the last 5 minutes rate(thoras_api_http_response_total[5m]) # Responses by status code sum by (code) (thoras_api_http_response_total) # Error rate (4xx and 5xx responses) sum(rate(thoras_api_http_response_total{code=~"4..|5.."}[5m])) # Requests by endpoint sum by (path, method) (thoras_api_http_response_total) # Success rate percentage (sum(rate(thoras_api_http_response_total{code=~"2.."}[5m])) / sum(rate(thoras_api_http_response_total[5m]))) * 100 ``` ### API HTTP Request Duration **Metric:** `thoras_api_http_request_duration_seconds` Measures the average response time for Thoras' internal API requests in seconds. Rising values may indicate performance degradation or capacity issues. **Example Queries & Dashboards:** ```promql theme={null} # Response time by endpoint avg by (path) (thoras_api_http_request_duration_seconds) # Average response time over the last hour avg_over_time(thoras_api_http_request_duration_seconds[1h]) ``` ### Forecast Queue Pending Count **Metric:** `thoras_forecast_queue_pending_count` **Labels:** `action` (`forecast` or `train`) Shows the current number of pending jobs waiting in the queue, broken down by action type. High values indicate that requests are backing up, which may signal worker capacity issues or processing bottlenecks. **Example Queries & Dashboards:** ```promql theme={null} # Current pending count by action thoras_forecast_queue_pending_count # Pending forecast jobs only thoras_forecast_queue_pending_count{action="forecast"} # Pending train jobs only thoras_forecast_queue_pending_count{action="train"} # Total pending jobs across all actions sum(thoras_forecast_queue_pending_count) # Maximum pending count over the last hour max_over_time(thoras_forecast_queue_pending_count[1h]) # Alert on high queue depth (> 10 jobs total) sum(thoras_forecast_queue_pending_count) > 10 ``` ### Forecast Queue Pending Oldest **Metric:** `thoras_forecast_queue_pending_oldest_duration_seconds` Tracks how long the oldest forecast job has been waiting in the queue. Rising values indicate jobs are not being processed quickly enough. **Example Queries & Dashboards:** ```promql theme={null} # Current oldest job wait time thoras_forecast_queue_pending_oldest_duration_seconds # Maximum wait time over the last day max_over_time(thoras_forecast_queue_pending_oldest_duration_seconds[24h]) # Average wait time over the last day avg_over_time(thoras_forecast_queue_pending_oldest_duration_seconds[24h]) ``` ### Forecast Queue Status Count **Metric:** `thoras_forecast_queue_status_count` Tracks the total number of forecast jobs processed, categorized by status. This counter helps monitor forecast job lifecycle and identify issues with job processing. **Example Queries & Dashboards:** ```promql theme={null} # Total forecast jobs by status sum by (status) (thoras_forecast_queue_status_count) # Rate of forecast jobs by status over the last day rate(thoras_forecast_queue_status_count[24h]) # Failed forecast jobs sum(thoras_forecast_queue_status_count{status="failed"}) # Success rate percentage (sum(rate(thoras_forecast_queue_status_count{status="completed"}[24h])) / sum(rate(thoras_forecast_queue_status_count[24h]))) * 100 # Total forecast jobs processed sum(thoras_forecast_queue_status_count) ``` ### Job Completions Total **Metric:** `thoras_job_completions_total` Tracks the total number of completed jobs, categorized by status (success, failure, etc.) and job kind. This counter helps identify job failure rates and patterns across different job types. **Example Queries & Dashboards:** ```promql theme={null} # Total job completions sum(thoras_job_completions_total) # Job completions by status sum by (status) (thoras_job_completions_total) # Job completions by kind sum by (kind) (thoras_job_completions_total) # Failed jobs only sum(thoras_job_completions_total{status="failure"}) # Job completion rate over the last hour rate(thoras_job_completions_total[1h]) # Failure rate percentage (sum(rate(thoras_job_completions_total{status="failure"}[24h])) / sum(rate(thoras_job_completions_total[24h]))) * 100 ``` ### Job Duration Seconds **Metric:** `thoras_job_duration_seconds` Measures the duration of completed jobs in seconds, broken down by status and job kind. Use this to identify slow jobs or performance regressions. **Example Queries & Dashboards:** ```promql theme={null} # Average duration by job kind avg by (kind) (thoras_job_duration_seconds) # Jobs taking longer than 30 seconds thoras_job_duration_seconds > 30 ``` ## Grafana Dashboard We provide a pre-built Grafana dashboard that visualizes Thoras metrics out of the box. It includes panels for scaling activity, provisioning efficiency, forecast pipeline health, API performance, and worker job status. Thoras Grafana Dashboard Thoras Grafana Dashboard ### Importing the Dashboard 1. Download the [Thoras Grafana Dashboard](./static/thoras-grafana-dashboard.json) 2. In Grafana, go to **Dashboards > New > Import** 3. Click **Upload dashboard JSON file** and select the downloaded file 4. When prompted, select your Prometheus datasource that scrapes Thoras metrics 5. Click **Import** ### Prerequisites * Thoras must be installed and exposing its Prometheus metrics endpoint * A Prometheus instance must be scraping Thoras metrics * Grafana must have that Prometheus instance configured as a datasource # OOM Remediation Source: https://docs.thoras.ai/guides/oom-remediation Automatically recover workloads from out-of-memory kill loops When a container is killed due to an out-of-memory (OOM) event, Thoras can automatically increase the container's memory request and limit to stabilize the workload. This prevents a workload from cycling through repeated OOM kills while waiting for a scheduled forecast to produce a sufficient memory recommendation. OOM remediation requires [autonomous vertical scaling](/guides/vertical-pod-rightsizing) to be enabled. It is opt-in per workload. ## How It Works When OOM kills are detected on a workload, Thoras: 1. **Bumps memory requests and limits** for affected containers by a 1.2x multiplier, computed independently from the average current memory request and limit across all running pods. Both request and limit are raised since the kernel OOM-kills based on the limit, and request guarantees node memory. 2. **Applies the adjustment** to running pods and any newly created pods. For running pods, Thoras uses your [`update_policy`](/reference/ast-definition#update_policy): it attempts an in-place resize first (if enabled). When that does not apply and your policy allows recreating pods, Thoras replaces the remaining pods gradually, in batches paced by your [`disruption`](/reference/ast-definition#disruption) settings, rather than restarting the whole workload at once. See [Rolling Out the Adjustment](#rolling-out-the-adjustment). Any pods created or rescheduled in the meantime (e.g., evictions, scale-out) automatically receive the adjusted memory. 3. **Repeats as long as OOM kills continue**, compounding each time (1.2×, then 1.44×, then 1.73×, and so on) until OOMs stop. Thoras waits at least 2 minutes between adjustments, and when the adjustment is rolled out in batches it also waits until every pod has the current adjustment before applying the next one. 4. **Holds the memory floor** for a configurable stabilization window after the last OOM. During this window, forecasts can raise memory above the floor but cannot lower it. This prevents a new forecast, which may not yet reflect the OOM episode, from reversing the adjustment down to a point which would be prone to OOMing. 5. **Returns full control to the forecaster** once the stabilization window expires. If OOMs recur, the cycle begins again. The adjustment is a short-term stabilization measure, not a permanent right-sizing decision. The goal is to keep the workload running long enough for the forecaster to observe the higher memory usage and incorporate it into future recommendations. ## Rolling Out the Adjustment New pods pick up the adjusted memory as soon as it is set. For pods that are already running, what Thoras does depends on your [`update_policy`](/reference/ast-definition#update_policy): | `update_mode` | In-place resize applies | In-place resize does not apply | | - | - | - | | `in_place_or_recreate` | Pods are resized | Remaining pods are replaced gradually in [`disruption`](/reference/ast-definition#disruption) batches | | `recreate` (or no `update_mode`) | Not attempted | Pods are replaced gradually in [`disruption`](/reference/ast-definition#disruption) batches | | `in_place` | Pods are resized | Pods are never recreated; Thoras retries the resize in [`disruption`](/reference/ast-definition#disruption) batches until it succeeds or the stabilization window ends | In-place resize "does not apply" when the resize fails (for example, the node lacks capacity), is not supported by the cluster, or includes a change the cluster can only make by restarting the pod (such as lowering a memory limit). Gradual replacement needs `memory` in [`recreate_resources`](/reference/ast-definition#update_policy), which is the default. If `recreate_resources` leaves out `memory` (for example `["cpu"]`), Thoras falls back to a rolling restart of the workload instead, when your `update_mode` allows recreating pods. When pods are replaced gradually: * **Batches follow your `disruption` settings.** Thoras evicts at most `max_disrupted` pods at a time and waits `interval` between batches. Evictions go through the Kubernetes eviction API, so your PodDisruptionBudgets are honored. Thoras does not modify your Deployment or StatefulSet, so GitOps tools see no drift. * **Crash-looping pods are replaced first.** A pod that is already down because it keeps OOMing goes in the next batch even when `max_disrupted` is used up, so it cannot stall the rollout. * **No forecast is needed.** The rollout proceeds even when the workload has no usable forecast yet, such as a new workload still collecting metrics. * **Only pods below the adjusted memory are replaced.** Your scale-up threshold does not apply here, and a pod that already has at least the adjusted memory is never replaced for the OOM adjustment. * **One adjustment at a time.** Pods still waiting for their turn may OOM again at the old memory. Those OOMs do not trigger another adjustment until every pod has the current one, so a single episode does not compound on top of itself while the rollout is still in progress. During a gradual replacement, you can follow progress with the same signals as any other paced rollout: the `DisruptionInProgress` condition on the AIScaleTarget, which reads `Complete` once every pod has the adjusted memory, and the `thoras_ast_disruption_completions_total` metric. See [In-Place Resize Behavior](/guides/in-place-resize) for the full list. ## Enabling OOM Remediation Add `oom_remediation` to your `spec.vertical` configuration: ```yaml theme={null} spec: vertical: mode: autonomous oom_remediation: enabled: true containers: - name: my-container memory: lowerbound: 256Mi upperbound: 4Gi ``` `oom_remediation.enabled` must be `true` and `spec.vertical.mode` must be `autonomous`. Workloads in `recommendation` mode are not affected. ## Stabilization Window After Thoras applies an OOM memory adjustment, it holds a **memory floor** for the duration of the stabilization window. During this window: * Incoming forecasts **cannot lower** memory below the adjusted value. * Forecasts **can raise** memory above the adjusted value if the forecast recommends it. * Each new OOM resets the stabilization window, extending it from the time of the most recent OOM. The workload must be stable for the configured interval before memory may be scaled down from the multiplied value. Once the window expires, Thoras clears the floor and the forecaster resumes full control of memory sizing. **Default duration:** `max(forecast_interval, 1h)`. The window is at minimum one full forecast cycle, ensuring the forecaster has had at least one opportunity to observe the workload at the adjusted memory level before the floor is removed. The stabilization window should be long enough for the forecaster to observe memory usage at the adjusted level, so future recommendations account for the higher usage that triggered the OOMs. To override the default, set `stabilization_window` explicitly: ```yaml theme={null} oom_remediation: enabled: true stabilization_window: 2h ``` ## Upper Bounds `memory.upperbound` is a **steady-state** policy and is intentionally bypassed during OOM remediation. OOM is an emergency response: clamping the adjustment to `upperbound` would leave the workload exposed to the same kill threshold that triggered the cycle in the first place. Adjustments may therefore land above the configured upper bound while the stabilization window is active. Once the stabilization window expires, the forecaster regains full control and steady-state recommendations are clamped to `upperbound` as usual. ## Memory Limits OOM remediation raises both the memory request and the memory limit. How the limit is determined depends on whether Thoras manages it: * **Limit managed by Thoras** (`memory.limit.ratio` is set): during the stabilization window, the limit is `max(adjusted_request × ratio, adjusted_limit)`. The ratio is preserved on the way up, and the OOM-adjusted limit acts as a floor so a non-OOM reconcile cannot snap the limit back below it mid-episode. After the window expires, the limit follows the ratio normally as the forecaster regains control. * **Limit not managed by Thoras** (no `memory.limit.ratio`): the limit is raised by the same multiplier as the request during the stabilization window to give the workload room to recover. When the window expires, Thoras reverts the limit to the value in your workload's pod template (deployment, statefulset, etc.). If your workload's true steady-state memory usage permanently exceeds the limit set in your pod template, OOMs will recur each time the limit reverts, visible as repeated `OOMKilled` events. The resolution is to raise the limit in the workload's pod template, or to configure `memory.limit.ratio` so Thoras can manage limits directly. ## Interaction with Forecasts During an active stabilization window, Thoras applies `max(forecast, floor)` to memory when scaling. CPU is always sourced from the forecast unchanged. When a new forecast arrives after the window has expired, the floor is cleared and the forecast value is applied directly. If the workload OOMs again, the remediation cycle restarts from the new forecast baseline. ## Considerations **Memory may be held above the forecast during the stabilization window.** If an OOM was caused by a one-off spike and the workload returns to lower memory usage afterward, requests will remain elevated until the window expires. Thoras favors avoiding another OOM over reclaiming memory immediately, and allows configuring the duration of the stabilization window. **New pods after the window expires start at the forecast value.** Once the floor clears, pods created from that point forward use the forecaster's recommendation. If that recommendation is insufficient, another OOM may occur and trigger a new remediation cycle. **Partial in-place resize failures.** If some pods cannot be resized in place (e.g., the node lacks capacity), Thoras replaces those pods gradually when your `update_policy` allows recreating them. With `update_mode: in_place`, they keep their previous memory until a later resize succeeds. **Recovery is paced by your `disruption` settings.** Replacing every pod takes roughly `replicas ÷ max_disrupted` batches, `interval` apart, and the next adjustment waits until the current one has reached every pod. A pod that a PodDisruptionBudget keeps from being evicted delays the rollout, and the next adjustment, until it can be replaced. For workloads that need to recover faster, raise `max_disrupted` or shorten `interval`. ## Full Example ```yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: my-service namespace: my-namespace spec: scaleTargetRef: kind: Deployment name: my-service apiVersion: apps/v1 model: mode: balanced forecast_interval: 1h forecast_blocks: 4h vertical: mode: autonomous oom_remediation: enabled: true stabilization_window: 2h update_policy: update_mode: in_place_or_recreate containers: - name: my-service memory: lowerbound: 256Mi upperbound: 4Gi cpu: lowerbound: 100m upperbound: 2 ``` In this configuration: * OOM remediation is enabled with a 2-hour stabilization window * Steady-state memory recommendations are clamped to 4 GiB (`upperbound`); OOM adjustments may exceed this temporarily during stabilization * Pods are resized in place where possible; otherwise they are replaced gradually in [`disruption`](/reference/ast-definition#disruption) batches # Pausing Autonomous Scaling Source: https://docs.thoras.ai/guides/pause-scaling Temporarily halt all autonomous scaling actions cluster-wide The scaling pause is a system-wide toggle that halts all autonomous scaling actions across the entire cluster. Thoras continues collecting metrics, generating predictions, and producing recommendations. No data or model state is lost while paused. Only [Autonomous mode](/guides/enabling-autonomous-mode) targets are affected. Workloads in `recommendation` mode are unaffected. ## Example Use Cases * **Planned maintenance**: prevent Thoras from modifying resource requests during a maintenance window * **Incident response**: freeze current allocations while investigating a production issue * **Baseline validation**: observe workload behavior without scaling, such as during load testing ## Pausing and Resuming Open the **Manage Cluster** dropdown in the dashboard header and select **Pause autonomous scaling**. A flyout displays the count of autonomous targets that will be affected and asks for confirmation before applying. To resume, open the same dropdown, selects **Resume autonomous scaling**, then confirm. The pause UI is gated behind `thorasDashboard.extras.enable_scaling_pause` in the Helm chart (default `true`). If you've turned it off, use the manual ConfigMap toggle described under [Advanced](#advanced). ## Pod Behavior **When you pause scaling**, workloads' running pods retain the resource requests last applied by Thoras. If a pod restarts while scaling is paused, it starts with the requests defined in the deployment spec, and Thoras will not apply any adjustments until scaling is resumed. **When you resume scaling** (un-pause), the Thoras operator restarts then reconciles autonomous targets, setting requests to the latest suggestion for pods which need resizing. ## Visual Indicators ### Banner When paused, a persistent amber banner appears at the top of the dashboard: > Autonomous scaling actions are paused. Targets will not be right-sized. The banner links directly to the resume flyout so you can re-enable scaling without navigating away. ### Scaling History A target's scaling and suggestions timeline indicates which suggestions were made while the system was in the scaling paused state, and which, if any, actions would have been applied had scaling not been paused. ## Persistence The pause state is stored in the `thoras-operator-system-config` ConfigMap in the Thoras namespace. If the ConfigMap does not exist, the system defaults to **not paused**. Toggling the state triggers a rolling restart of both the operator and API server to load the new configuration. There is a brief period of unavailability for the API server and operator as they restart. Because the state lives in a ConfigMap, **it survives pod restarts and upgrades**. The cluster remains paused until explicitly resumed. ## Advanced As an alternative to using the dashboard, scaling pause state can be toggled manually by modifying the ConfigMap `thoras-operator-system-config` then restarting `thoras-api-server` and `thoras-operator`. # Stopping Autonomous Scaling Source: https://docs.thoras.ai/guides/stopping-autoscaling Options for halting Thoras autoscaling, from cluster-wide to per-workload There are three ways to stop Thoras from autonomously scaling workloads, ordered from broadest to narrowest scope. Choose the option that matches the blast radius you need. | Approach | Scope | Restarts Pods | Workload requests | Model & data retention | | - | - | - | - | - | | System-wide pause | All autonomous ASTs | No | Running pods keep the last applied suggestion; restarted pods come up with the controller spec | Thoras keeps forecasting and producing suggestions | | `recommendation` mode | One AST | Yes | Reverts to the controller spec | Thoras keeps forecasting and producing suggestions | | Unenroll (delete AST) | One AST | Yes | Reverts to the controller spec | The workload is no longer managed or observed by Thoras. A recreated AST starts with empty history unless it [inherits it](/reference/ast-definition#keeping-history-when-an-ast-is-recreated) | ## 1. System-wide pause Halts all autonomous scaling actions across the entire cluster. Thoras continues collecting metrics and producing recommendations; only the apply step is suspended. Workloads in `recommendation` mode are unaffected. Pausing does **not** restart workload pods. Running pods keep the resource requests Thoras last applied to them. When new pods are created they will use the requests defined in the controller spec. **When to use:** planned maintenance windows, incident response, or baseline validation where you want to freeze allocations cluster-wide without changing any `AIScaleTarget` definitions. **How to do it:** 1. Open the **Manage Cluster** dropdown in the dashboard header. 2. Select **Pause autonomous scaling**. 3. Confirm in the flyout, which shows the count of autonomous targets that will be affected. To resume, open the same dropdown and select **Resume autonomous scaling**. The pause state persists across pod restarts and upgrades via the `thoras-operator-system-config` ConfigMap. See [Pausing Autonomous Scaling](/guides/pause-scaling) for pod behavior during pause, visual indicators, and the ConfigMap-based advanced workflow. ## 2. Switch the AST from autonomous to recommendation mode Stops autonomous scaling for a single workload while keeping the `AIScaleTarget` enrolled. Thoras continues to forecast and surface suggestions in the dashboard, but no scaling actions are applied. Unlike a system-wide pause, switching to `recommendation` mode restarts the workload's pods and reverts it to the requests defined in the controller spec. **When to use:** you want to keep the workload monitored and the model warm (for example, to compare suggestions against actual usage), but you don't want Thoras applying changes for that specific workload. **How to do it:** Edit the `AIScaleTarget` and set the active scaling direction's `mode` to `recommendation`: ```sh theme={null} kubectl edit ast {{YOUR_AST_NAME}} -n {{YOUR_NAMESPACE}} ``` For a vertically autonomous workload: ```yaml theme={null} spec: vertical: mode: recommendation ``` For a horizontally autonomous workload: ```yaml theme={null} spec: horizontal: mode: recommendation ``` Recommendations continue to appear in the dashboard. To re-enable autonomous scaling later, set `mode` back to `autonomous`. See [Understanding Vertical and Horizontal Scaling Modes](/guides/configuring-asts#understanding-vertical-and-horizontal-scaling-modes) for how the two directions interact, and the [AIScaleTarget reference](/reference/ast-definition) for the full mode specification. ## 3. Unenroll the workload (delete the AST) Removes the workload from Thoras entirely. The workload is restarted and its pods come up with the resource requests defined in the controller spec. **Deleting an AST is permanent.** Its model state is gone, and Thoras stops using its suggestions and utilization data, which age out under normal retention. Re-creating an AST with the same name later produces a new AST with empty history, unless you create it with the `thoras.ai/inherit-history` annotation to [keep the retained history](/reference/ast-definition#keeping-history-when-an-ast-is-recreated). If you want to stop scaling actions while keeping history intact, use option 1 or 2 above. **When to use:** the workload should no longer be managed or observed by Thoras at all. For example, when the workload is being decommissioned, moved to a different scaling system, or you want a clean re-enrollment later. **How to do it:** ```sh theme={null} kubectl delete ast {{YOUR_AST_NAME}} -n {{YOUR_NAMESPACE}} ``` If the AIScaleTarget is managed by a `ClusterAIScaleTemplate` (it has the `thoras.ai/managed-by-template` label), deleting it isn't enough — the template will recreate it on the next reconcile. Annotate the workload with `thoras.ai/skip-template: "true"` first, then delete the AIScaleTarget. See [Opting a Workload Out](/reference/cluster-aiscale-template#opting-a-workload-out) for details. # Predictive Vertical Pod Rightsizing Source: https://docs.thoras.ai/guides/vertical-pod-rightsizing Predictive Vertical Pod Rightsizing is the process of preemptively optimizing the CPU and memory resource requests and limits for Kubernetes pods to ensure they have just enough resources to run efficiently without over or under provisioning. Unlike horizontal scaling, which adjusts the number of pod replicas, vertical rightsizing focuses on tuning individual pod configurations to better match their actual usage patterns. This helps reduce wasted resources, lower cloud costs, and improve cluster utilization, while also minimizing the risk of resource starvation or out-of-memory (OOM) errors. **In-place pod resize on Kubernetes 1.33+.** When running on Kubernetes 1.33 or later, Thoras applies CPU and memory adjustments without restarting pods, eliminating the rollout churn historically required by vertical autoscalers. Resizes are still rolled out in paced batches across the workload rather than all at once. Thoras falls back to a rolling restart automatically on older clusters or when an in-place resize is not supported (for example, a QoS class change). See [Update Policy](#update-policy) for details. ## Prerequisites Before you begin, make sure: * Thoras is installed and running in your Kubernetes cluster. * The [Kubernetes Metrics Server](https://github.com/kubernetes-sigs/metrics-server) is installed in your cluster. * Your workloads are instrumented with resource requests and have some usage history. ## Sample Configuration Here's an example `AIScaleTarget` that configures rightsizing. ```yaml ast.yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} spec: scaleTargetRef: kind: Deployment name: {{YOUR_DEPLOYMENT_NAME}} model: forecast_blocks: 4h forecast_interval: 1h forecast_buffer_percentage: 0% mode: balanced vertical: mode: recommendation containers: - name: {{YOUR_CONTAINER_NAME}} cpu: lowerbound: 20m upperbound: 1 memory: lowerbound: 50Mi limit: ratio: 1.5 update_policy: update_mode: in_place_or_recreate ``` ## Vertical Breakdown * `spec.vertical.mode` set to `recommendation` ensures Thoras suggests optimal requests and limits but does not apply them. * `spec.vertical.containers[].name` should be set to the name of the container in the pods for the workload you are targeting with this `AIScaleTarget`. This field supports wildcard patterns (e.g., `app-*`, `*-sidecar`) to match multiple containers. See [Container Name Wildcard Matching](/reference/ast-definition#container-name-wildcard-matching) for details. * `spec.vertical.containers[].RESOURCE.lowerbound` is the lowest Thoras is allowed to set the request for that resource and is a *required* field. * `spec.vertical.containers[].RESOURCE.upperbound` is the highest Thoras is allowed to set the request for that resource and is an optional field. * In `autonomous` mode, if you change `lowerbound` or `upperbound` and live pod requests fall outside the new range, Thoras applies the updated bounds immediately according to [`update_policy`](#update-policy) (in-place resize and/or recreate). With `update_mode: initial`, bounds still apply only when pods are next created. * `spec.vertical.containers[].RESOURCE.limit` allows thoras to modify the container's limit along with the request for this resource and is optional. If unset, Thoras will *not* modify limits for this resource. * `spec.vertical.containers[].RESOURCE.limit.ratio` the ratio used to keep the limit inline with the suggested request for this resource. E.g. for a ratio `2.5` Thoras will make the limit `2.5G` if the suggestion for the request was for `1G`. ## How Limits Work With Predictive Right-Sizing When Thoras right-sizes your pods, it respects existing resource limits to ensure safe operation. Here's how limits interact with Thoras recommendations: ### Limit Enforcement Rules Thoras enforces the following maximum constraints when right-sizing: * **AIScaleTarget `upperbound`**: If you define an `upperbound` in your AIScaleTarget, Thoras will never set requests to above that value during steady-state operation. * **Pod spec `limit`**: If a `limit` exists on the pod spec, Thoras will never right-size a pod above that limit during steady-state operation. When both a pod `limit` and an AIScaleTarget `upperbound` are set, Thoras uses the lower of the two as the effective maximum value for right-sizing. `upperbound` is a steady-state constraint. The one exception is [OOM remediation](/guides/oom-remediation), an opt-in emergency response that intentionally bypasses `memory.upperbound` so it can lift a workload out of an out-of-memory kill loop. Steady-state recommendations are clamped to `upperbound` as usual once the remediation's stabilization window expires. Changing `lowerbound` or `upperbound` on an autonomous AIScaleTarget also re-enforces those bounds on running pods that are outside the new range. See [Vertical Breakdown](#vertical-breakdown) and [`update_policy`](#update-policy). ### Scaling Limits Proportionally You can configure Thoras to automatically scale your resource limits along with your requests using the `limit.ratio` setting. This keeps limits proportional to requests as Thoras adjusts resource allocations. For example, setting `.spec.vertical.containers[].RESOURCE.limit.ratio: 1.5` means that if Thoras recommends a request of `1G`, the limit will be set to `1.5G`. See the [AIScaleTarget reference documentation](/reference/ast-definition#vertical) for more details on configuring the `limit.ratio` field. ## Update Policy The `update_policy` field controls pod update behavior during vertical scaling operations (such as restarts). **Configuration:** * `update_mode` - Determines how pod updates are applied: * `recreate` - Pods are recreated when resource changes are applied * `in_place` - Changes applied without pod recreation (requires Kubernetes 1.33+) * `in_place_or_recreate` - Attempts in-place updates with fallback to recreation (recommended) * `initial` - Recommendations apply only during pod creation * `recreate_resources` (optional; defaults to `memory` only) - Array specifying which resources (`memory`, `cpu`) trigger pod recreation when using recreate or fallback modes. **Example configurations:** For in-place updates with fallback to recreation: ```yaml theme={null} update_policy: update_mode: in_place_or_recreate ``` For in-place-only updating, no fallback to recreation: ```yaml theme={null} update_policy: update_mode: in_place recreate_resources: ["memory"] ``` For in-place updates with fallback to recreation if either `memory` or `cpu` require right sizing: ```yaml theme={null} update_policy: update_mode: in_place_or_recreate recreate_resources: ["memory", "cpu"] ``` > **Note:** Enabling in-place resizing does not guarantee that pods will never > restart during resizing operations. Pods may still be evicted and restarted if > the resize would change the pod's > [QoS class](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/) or if > the cluster does not support the specific resize operation. **How it works:** 1. When `update_mode` is `in_place` or `in_place_or_recreate`, Thoras attempts to resize pods in place using the Kubernetes resize API. Resizes are rolled out in paced batches across the workload rather than all at once, governed by [`disruption`](/reference/ast-definition#disruption). If the Kubernetes API rate-limits these requests, Thoras backs off and resumes the remaining pods on a later run 2. When `update_mode` is `in_place_or_recreate` and the pods [QoS class](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/) would change or resize is not supported, Thoras falls back to recreating the pod 3. Evicted pods are also rolled out gradually to minimize service disruption For details on what causes a resize to stall and how each mode responds, see [In-Place Resize Behavior](/guides/in-place-resize). **Benefits:** * **Reduced disruption**: Pods continue running during resource adjustments * **Faster scaling**: Changes take effect immediately without pod recreation * **Better node utilization**: Enables more efficient bin-packing and cost savings, especially when combined with node autoscaling tools like Karpenter * **Graceful fallback**: Automatic eviction when in-place resizing isn't possible * **Fine-grained control**: Specify which resources trigger pod recreation **Requirements:** * Kubernetes 1.33+ ## Additional Details * Thoras recommendations are based on historical usage data collected from the Kubernetes Metrics Server. * Recommendations are updated on the interval defined by `spec.model.forecast_interval`. * If resource usage patterns change significantly, Thoras will adjust its suggestions accordingly. * You can monitor the impact of rightsizing by reviewing resource utilization and cost metrics in the Thoras Dashboard. * For workloads with multiple containers, you can specify vertical settings for each container individually under `spec.vertical.containers`. * `spec.vertical.containers[].RESOURCE` can be either `cpu` or `memory`. If either is unset for a container, Thoras will not scale that resource for that container. ## Viewing Recommendations Once the `AIScaleTarget` is applied, The Thoras Reasoning Engine will begin analyzing resource usage and generate vertical sizing recommendations on the interval defined by `spec.model.forecast_interval`. You can view the suggested requests and their projected effects on your workload in the Thoras dashboard under Resource Allocation Settings. resource-allocation ## Next Steps * Change `spec.vertical.mode` to `autonomous` if you want Thoras to automatically apply recommendations. * Add optional `scaling_behavior` if you want to fine-tune the thresholds Thoras uses to determine if a recommendation is worth restarting the entire workload. ```yaml ast.yaml theme={null} ... vertical: mode: autonomous # Only restart workload if a new resource recommendation is at least 50% greater or lower than the current pod resource allocation scaling_behavior: scale_up: type: percent percent: 50 scale_down: type: percent percent: 50 ... ``` ## Autonomous Mode The Thoras Reasoning Engine will make usage predictions on the interval set by `spec.model.forecast_interval` for the length of `spec.model.forecast_blocks`. If no `spec.vertical.scaling_behavior` is set then Thoras will check all resource requests to determine if they match the current suggestion. If any resource request differs from the current suggestion, Thoras triggers a rollout restart of the workload. As the pods restart, their resource requests and limits will be updated to match the suggestion made by Thoras. > **Note:** Thoras mutates the resource requests and limits of running pods > directly. It does **not** modify the Deployment or StatefulSet object itself. > **Note:** Only one scaling direction (horizontal or vertical) can be in > `autonomous` mode at a time. See > [Configuring AIScaleTargets](./configuring-asts#understanding-vertical-and-horizontal-scaling-modes) > for additional details. ## Identifying Thoras-Managed Pods When the admission webhook sets a pod's resources in `autonomous` mode, it annotates the pod with `thoras.ai/managed: "true"`, `thoras.ai/scaler`, and `thoras.ai/admitted-at`. This lets external tooling identify pods Thoras has mutated without diffing against the workload's controller spec. See [Pod Annotations](/reference/pod-annotations) for the full reference, including limitations around pods that predate autonomous mode and pods that have since been reverted to template values. # Advanced Setup Source: https://docs.thoras.ai/installation/advanced-setup This page collects the Helm chart knobs you're most likely to touch beyond a default install. For a quicker, higher-level guide to install and try out Thoras, check out the [QuickStart Guide](../getting-started/quickstart). Several install-time topics live on their own pages: * [Persistent Storage](/installation/persistent-storage) – configuring the metrics-collector PV and, optionally, an external TimescaleDB. * [Credentials](/installation/credentials) – how to supply and rotate the credentials Thoras uses. * [Private Container Registry](/installation/private-registry) – pulling images from your own registry, including air-gapped installs. ## Installing the Thoras license Thoras will provide you a license key which should be made available to your installation. You have two options for managing this value: 1. Pass the value into the Helm release via `$.Values.imageCredentials.password`, or 2. Reference an existing secret Let's review both options, choose whichever works best for you! ### Option 1: Pass license in as Helm chart value Pass the content of your license key file to `$.Values.imageCredentials.password` in your helm release ### Reference an existing secret If you'd prefer to reference a pre-existing secret object instead of passing values into your helm release, this option is for you! Ensure a secret of type `docker-registry` exists in the `thoras` namespace and that it's structured correctly: 1. An example of how to create the secret manually: ```sh theme={null} kubectl create secret docker-registry thoras-license \ --docker-server=us-east4-docker.pkg.dev/thoras-registry/platform \ --docker-username=_json_key_base64 \ --docker-password="{{ YOUR_LICENSE_KEY_CONTENT_HERE }}" \ -n thoras ``` 2. In the helm values file, reference this secret by setting `.Values.imageCredentials.secretRef: "thoras-license"`. ### Confirm license installed successfully You can confirm the license has been installed successfully if the pods in the `thoras` namespace come up into `Running` status after one or two minutes. Pod error statuses such as `ImagePullBackOff` might indicate an issue. ## Forecast worker capacity Thoras runs a pool of forecast workers that produce predictions for your enrolled workloads. By default the chart runs a built-in scaler that grows the pool automatically as you enroll more `AIScaleTargets`, so most installs need no configuration here. **How the built-in scaler works** * Scaler settings live under `thorasForecast.worker.scaler`: * `minReplicas: 1`: the floor. * `maxReplicas: 100`: the ceiling. * `astsPerReplica: 67`: the target ratio. The scaler adds a worker replica for roughly every 67 `AIScaleTargets`. * `interval: "1m"`: how often the scaler reconciles. * `thorasForecast.worker.replicas` is intentionally **unset** by default so the scaler is in charge. **Pinning a fixed number of workers** Setting `thorasForecast.worker.replicas` to an explicit number disables the built-in scaler and pins the pool at that size. Use this only if you specifically want a static count (for example, to match a capacity plan or work around a scaler issue): ```yaml theme={null} thorasForecast: worker: replicas: 3 ``` ## Configuring Webhook Namespace Exclusions By default, Thoras webhooks exclude the `kube-system` and `kube-node-lease` namespaces. You can customize which namespaces are excluded, or disable exclusions entirely, using the `thorasOperator.webhooks.namespaceSelector` Helm value. ### Default Behavior Out of the box, Thoras excludes system namespaces from webhook enforcement: ```yaml theme={null} thorasOperator: webhooks: namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: - kube-system - kube-node-lease ``` ### Customizing Excluded Namespaces To exclude additional namespaces (e.g. a monitoring namespace), extend the `values` list: ```yaml theme={null} thorasOperator: webhooks: namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: - kube-system - kube-node-lease - monitoring ``` ### Disabling Namespace Exclusions If you use an admission enforcer such as OPA Gatekeeper that manages namespace selection independently, you can disable the default exclusions by setting the selector to `null` so the chart omits it entirely: ```yaml theme={null} thorasOperator: webhooks: namespaceSelector: null ``` ## Webhook TLS with cert-manager By default the chart provisions its own webhook TLS material via a bundled cert-generator Job. If you already run [cert-manager](https://cert-manager.io) in your cluster, you can hand that job off to cert-manager instead: ```yaml theme={null} thorasOperator: webhookCertGen: certManager: enabled: true ``` With this enabled, cert-manager provisions the `thoras-tls-certs` Secret and injects the CA bundle into the webhook configurations. The bundled kube-webhook-certgen Jobs, along with their RBAC and network policy, are skipped. This is the recommended setting for GitOps tools such as Argo CD and Flux, and for any `helm template` based workflow, since Helm hook Jobs are handled poorly outside of `helm install`. The chart uses a long-lived CA (10 years by default) that signs the webhook leaf cert, so routine leaf renewals don't rotate the injected CA bundle. Adjust the CA lifetime via `thorasOperator.webhookCertGen.certManager.ca.duration` if your policy requires it. ## Scheduling The chart exposes standard Kubernetes scheduling primitives at both a global level and per-component. Component-specific settings take precedence over global ones. **Global (applied to every component and to the chart's hook Jobs):** ```yaml theme={null} nodeSelector: kubernetes.io/os: linux tolerations: - key: dedicated operator: Equal value: thoras effect: NoSchedule # Global affinity rules that components can opt into via useGlobalAffinity: true affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: node.kubernetes.io/instance-type operator: In values: [m5.large, m5.xlarge] priorityClassName: thoras-high-priority # Applied to all components; a component-specific list fully replaces this. topologySpreadConstraints: - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway ``` **Per-component overrides** exist under each component block (`thorasOperator`, `thorasApiServerV2`, `thorasWorker`, `thorasForecast`, `metricsCollector`, `thorasDashboard`, `thorasConfigController`) as `affinity`, `priorityClassName`, and `topologySpreadConstraints`. Each component also has a `useGlobalAffinity` switch (default `true`) that controls whether the global `affinity` block is merged in on top of the component's own. ## NetworkPolicy The chart can emit a least-privilege network policy for every Thoras component, in one of two flavors: ```yaml theme={null} networkPolicy: enabled: true # kubernetes (default): standard networking.k8s.io/v1 NetworkPolicy, works # with any NetworkPolicy-capable CNI. # cilium: CiliumNetworkPolicy (cilium.io/v2). Requires Cilium. flavor: kubernetes ``` **Kubernetes API server ports.** Standard `NetworkPolicy` is evaluated after kube-proxy translates the `kubernetes` Service address to a real endpoint, so the `kubernetes` flavor needs to know the port the API server actually listens on (`networkPolicy.apiServerPorts`, default `[443, 6443]`). Check with `kubectl get endpoints kubernetes -n default` and add the port if it's non-standard. Minikube, for example, uses `8443`. The `cilium` flavor targets the API server by identity and ignores this list. **Extending the policies.** Each component block accepts `extraIngressRules` and `extraEgressRules`, appended verbatim to the generated policy in either flavor. **OIDC egress.** When [dashboard OIDC auth](/guides/dashboard-auth) is enabled, the chart opens egress on port `443` from the dashboard pod so the sidecar can reach your identity provider. Tighten with a layered policy scoped to your IdP's hostnames if required. ## Egress proxy If Thoras components must dial an outbound HTTP/HTTPS proxy, for example to reach `console.thoras.ai` for cloud sync, set the standard proxy environment variables globally: ```yaml theme={null} proxy: httpProxy: "http://proxy.example.com:3128" httpsProxy: "http://proxy.example.com:3128" noProxy: ".svc.cluster.local,10.0.0.0/8" ``` The values are injected into every Thoras component as `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. ## Prometheus scraping Every Thoras component that emits Prometheus metrics has a `prometheus.enabled` toggle (default `true`) and a fixed `prometheus.port`. The chart can also emit a `ServiceMonitor` for the Prometheus Operator via the `serviceMonitor` values. See [Monitoring Thoras](/guides/metrics) for the full scraping configuration and the list of metrics. # Credentials Source: https://docs.thoras.ai/installation/credentials How Thoras handles the credentials it needs, and how to supply your own. Thoras needs a handful of credentials to run: an internal API token, the dashboard login, and the bundled database password. You don't have to provide any of them. On first install, Thoras generates whatever you didn't supply and stores it in the `thoras-config-controller` Secret in the release namespace. If a credential later changes, Thoras restarts the components that use it so you don't have to. On a fresh install, workload pods briefly report `CreateContainerConfigError` while Thoras performs its first reconcile. This clears on its own. ## Supplying your own credentials Each credential resolves in this order of precedence: 1. **A Secret you manage**, referenced via `*.existingSecret` or `*SecretRef*` values. 2. **A value pinned in `values.yaml`**. 3. **Generated by Thoras** into the `thoras-config-controller` Secret (default). For example, to point the internal API token at your own Secret: ```yaml theme={null} apiClientSecret: existingSecret: secretName: my-thoras-api-client secretKey: api-client-secret ``` The Helm chart README lists every credential and the values keys that back it. See [Secrets](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#secrets) for the full table. ## Rotating a credential Generated values are not intended to be rotated in place. To control rotation, take ownership of the value by pinning it in `values.yaml` or by pointing the chart at a Secret you manage. * **Existing Secret:** update the value in your Secret. Thoras picks up the change within about a minute. * **Pinned value:** change it in `values.yaml` and run `helm upgrade`. Two caveats: * Rotating the dashboard cookie secret signs every active dashboard session out. * The bundled TimescaleDB password has no rotation path. Use [external TimescaleDB](/installation/persistent-storage#external-timescaledb-byodb) if you need to rotate the database password. For the exact rotation semantics under GitOps, see [Rotating secrets](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#rotating-secrets) in the chart README. ## Legacy secret seeding `featureFlags.enableLegacySecretSeeding` (default `true`) keeps rendering the chart 4.x `api-client-secret` and `thoras-timescale-password` Secrets so their values can be migrated on upgrade. Nothing consumes them afterwards. * **Fresh installs:** set it to `false`. * **Upgrading from 4.x:** leave it on for the upgrade, then set it to `false` once the pods are healthy. * **GitOps and `helm template`:** set it to `false`. The legacy Secrets use Helm `lookup`, which those workflows cannot evaluate. The flag and the legacy Secrets are removed in chart 6.0. See [Deprecations](/deprecations). ## Further reading * Chart README: [Secrets](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#secrets), [Rotating secrets](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#rotating-secrets), and [Thoras Config Controller values](https://github.com/thoras-ai/helm-charts/tree/main/charts/thoras#thoras-config-controller). * [Default (htpasswd) dashboard auth](/guides/dashboard-auth/htpasswd) * [ArgoCD](/guides/argo-cd) # Persistent Storage Source: https://docs.thoras.ai/installation/persistent-storage Configure the persistent volume Thoras uses to retain historical usage metrics, and optionally point Thoras at an external TimescaleDB. To produce production-grade predictions, Thoras needs durable storage for historical usage metrics. The chart supports two independent pieces of state: 1. A Kubernetes [persistent volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) (PV) attached to the metrics collector. 2. A TimescaleDB instance for time-series storage. The chart deploys one in-cluster by default; you can point it at an external database instead. ## Understanding Storage Requirements You'll want to ensure Thoras has the disk space it needs now and in the future. This may or may not require configuration based on your backend storage provider. ### Understanding Usage The storage requirement for a managed workload is `1GB`. So for a cluster with twenty managed workloads, Thoras requires `20GB` of disk space. ### Understanding Kubernetes Storage Backend In general there are two patterns for Kubernetes storage backends with regard to disk space configuration: #### Growable Storage Backends These are PV storage backends that don't enforce a fixed size, they dynamically grow as-needed and require no Thoras configuration to specify volume size. Examples include: * `AWS Elastic File System (EFS)` * `Google Cloud Filestore (enterprise tier)` * `Azure Files` * `CephFS` **Action:** If you use one of these backends, you don't need to allocate disk space in the Thoras configuration, the disk will automatically grow to meet your needs. #### Fixed-Size Storage Backends These are PV storage backends that require explicit provisioning of a fixed capacity and enforce the size specified in the `PersistentVolumeClaim`. These backends require you to specify the desired size of the disk, which you should configure as a Helm chart value (read on for details on how to do that). Examples include: * `AWS Elastic Block Store (EBS)` * `Google Persistent Disk (GCP PD)` * `Azure Disk Storage` * `VMware vSphere Volumes` **Action:** If you use a fixed-size storage backend, you'll need to specify the volume size as a Helm value: 1. Determine how much total size will be needed using [guidance above](#understanding-usage) 2. Configure the storage size by setting the following helm value: ```yaml theme={null} metricsCollector: persistence: # Or whatever size needed based on your math from step #1 pvcStorageRequestSize: "100Gi" ``` ## Managing Persistent Volumes You may have an established workflow for provisioning Kubernetes Persistent Volumes (PV). That's great! Here's how to get up and running depending on your preferred approach: ### Referencing an existing `StorageClass` provisioner If you have a Kubernetes `StorageClass` that you'd like to use to dynamically provision your PV (such as an [EFS StorageClass](https://github.com/kubernetes-sigs/aws-efs-csi-driver/blob/master/examples/kubernetes/dynamic_provisioning/specs/storageclass.yaml)). In that case, simply reference the storage class name in your Thoras helm chart `values.yaml`: ```yaml theme={null} metricsCollector: persistence: storageClassName: "{{ YOUR_STORAGE_CLASS_NAME }}" ``` `metricsCollector.persistence.enabled` defaults to `null`, which auto-enables persistence as soon as you set any of `storageClassName`, `pvcStorageRequestSize`, or `createEFSStorageClass.fileSystemId`. Setting `enabled: true` is only necessary if you want to force persistence on without configuring any of the above. ### Referencing an existing PV If you have a workflow for provisioning a PV and would like to simply reference the PV directly by name, add the following config block to your Helm `values.yaml`: ```yaml theme={null} metricsCollector: persistence: enabled: true volumeName: "{{ YOUR_VOLUME_NAME_HERE }}" ``` ## External TimescaleDB (BYODB) The chart deploys an in-cluster TimescaleDB by default. If you'd rather manage TimescaleDB yourself, for backups, HA, or shared infrastructure, point Thoras at an external database instead: ```yaml theme={null} externalTimescale: # Full postgres DSN, including the database name. dsn: "postgres://user:password@host:5432/tsdb?sslmode=require" ``` Or reference an existing Secret containing the DSN: ```yaml theme={null} externalTimescale: secretRefName: my-timescale-dsn secretRefKey: dsn ``` When either is set, the chart skips deploying its bundled TimescaleDB and wires all components to the external database. The TimescaleDB extension must be pre-installed on the external database. See [Credentials](/installation/credentials) for how the DSN interacts with the other credentials Thoras manages. ## Questions? For help, email [support@thoras.ai](mailto:support@thoras.ai). # Private Container Registry Source: https://docs.thoras.ai/installation/private-registry Pull Thoras container images from your own private registry instead of the public Thoras registry. If you need to pull Thoras container images from your own private registry instead of the public Thoras registry, you can override the default registry location using the `$.Values.imageCredentials.registry` value. ## Prerequisites 1. Access to download Thoras container images (email [support@thoras.ai](mailto:support@thoras.ai) for assistance) 2. A private container registry accessible from your Kubernetes cluster 3. Container images pushed to your private registry ## Configuration Set the private registry hostname value in your Helm `values.yaml`: ```yaml theme={null} imageCredentials: registry: "your-private-registry.example.com/thoras" ``` ## Copying Multi-Platform Images Thoras container images are built for multiple platforms (`linux/amd64` and `linux/arm64`) to support multiple CPU architectures. When copying images from the Thoras registry to your private registry, it's a good idea to preserve the multi-platform manifest to ensure compatibility across different CPU architectures. ### Generate a list of images to copy ```sh theme={null} helm repo add thoras https://thoras-ai.github.io/helm-charts helm template testit thoras/thoras \ --set featureFlags.enableLegacySecretSeeding=false \ | awk '/image:/ {print $NF}' | sort -u ``` The list will include Thoras images (`services`, `thoras-dashboard-v2`, `thoras-forecast`, `nginx`, `timescaledb`) as well as `oauth2-proxy` and `httpd` (the dashboard auth sidecar and its init container) and `kube-webhook-certgen` (webhook TLS, unless you use cert-manager). All are hosted on the Thoras registry, so air-gapped installs only need to mirror from the Thoras registry. No separate upstream mirror step is needed. If you deploy with `helm template` rather than `helm install`, set `featureFlags.enableLegacySecretSeeding: false` in your values (see [Legacy secret seeding](/installation/credentials#legacy-secret-seeding)) and consider [cert-manager for webhook TLS](/installation/advanced-setup#webhook-tls-with-cert-manager) so no Helm hook Jobs are involved. ### Using Docker Buildx The recommended approach is to use `docker buildx imagetools create`, which preserves the multi-platform manifest. Substitute `` with the tag that `helm template` shows for the release you're deploying: ```sh theme={null} # Authenticate to both registries docker login us-east4-docker.pkg.dev/thoras-registry/platform docker login your-private-registry.example.com # Copy the image with all platforms preserved docker buildx imagetools create \ --tag your-private-registry.example.com/thoras/services: \ us-east4-docker.pkg.dev/thoras-registry/platform/services: ``` ### Verifying Multi-Platform Support After copying, verify that both platforms are available: ```sh theme={null} docker buildx imagetools inspect your-private-registry.example.com/thoras/services: ``` You should see both `linux/amd64` and `linux/arm64` in the output. ## Questions? For help downloading container images or configuring your private registry, email [support@thoras.ai](mailto:support@thoras.ai). # System Requirements Source: https://docs.thoras.ai/installation/system-requirements * **Kubernetes**. It doesn't matter which cloud, managed vs unmanaged, or on-prem. If it's Kubernetes, Thoras has your back! Thoras supports the five most recent upstream Kubernetes minor versions. See the [support policy](/policies/support-policy#kubernetes-version-support). * **Kubernetes metrics server**. Thoras reads live resource usage from the [metrics server](https://github.com/kubernetes-sigs/metrics-server). Most clusters already have it. * **A Persistent Volume**. For production-grade predictions, Thoras requires a [Kubernetes persistent volume](./persistent-storage). Note that a PV is not required for a demo or trial context! Thoras is extremely lightweight and a small install runs on about 2 vCPU. Forecast capacity scales itself as you enroll workloads. See [Forecast worker capacity](./advanced-setup#forecast-worker-capacity) for details. Clusters with 5,000 or more enrolled workloads should review [Tuning for Large Clusters](/guides/large-clusters). ## Optional integrations * **cert-manager**, to provision the admission webhook certificates declaratively instead of via Helm hook Jobs. Recommended for GitOps. See [Webhook TLS with cert-manager](./advanced-setup#webhook-tls-with-cert-manager). * **Cilium**, if you want the chart's network policies rendered as `CiliumNetworkPolicy`. Standard `NetworkPolicy` works with any capable CNI. See [NetworkPolicy](./advanced-setup#networkpolicy). * **An OIDC identity provider** (Okta, Azure Entra ID, or any OIDC-compliant provider) for dashboard single sign-on. See [Dashboard authentication](/guides/dashboard-auth). * **A private container registry**, if you're running in an air-gapped environment or prefer to mirror Thoras images internally. See [Private Container Registry](./private-registry). # Upgrading Source: https://docs.thoras.ai/installation/upgrading How Thoras Helm chart versions map to upgrade effort, and what changed in chart 5.x. Thoras ships continuously. Within a single major version (for example `5.x.x`), minor and patch releases are routine and should not require any special steps. A major chart version bump (for example `4.x.x` to `5.x.x`) signals a breaking change that may need human intervention. Only one major version can be crossed at a time (for example, `4.x.x` to `6.x.x` is not supported). The authoritative, step-by-step migration notes for every major version live in the chart repository: Detailed upgrade procedures for each breaking chart release. ## Chart 5.x at a glance Chart `5.0` adds dashboard authentication and moves credential management into the cluster. Most `4.x` installs upgrade with no changes to `values.yaml`. * **The dashboard requires a login.** An oauth2-proxy sidecar now fronts the dashboard, using a generated username and password by default or your identity provider via OIDC. See [Dashboard authentication](/guides/dashboard-auth). * **Credentials are managed in-cluster.** A new `thoras-config-controller` component seeds any credential you don't supply, migrates your existing 4.x credentials automatically, and restarts dependent workloads when a credential changes. See [Credentials](/installation/credentials). * **`featureFlags.enableSimpleAuthSecret` is renamed** to `apiClientSecret.enabled`. The old name still works as an alias. See [Deprecations](/deprecations). ## Does anything apply to me? Work through whichever of these match your deployment: | Your 4.x setup | What to do | | - | - | | Dashboard reached via `kubectl port-forward` or an ingress with no auth in front | Nothing before upgrading. Afterwards, sign in with the generated credentials. See [Default (htpasswd) auth](/guides/dashboard-auth/htpasswd). | | Your own oauth2-proxy under `thorasDashboard.extraContainers` | Move to the built-in sidecar. Your IdP app and Secret carry over. See [Migrating from a hand-rolled sidecar](/guides/dashboard-auth#migrating-from-a-hand-rolled-oauth2-proxy-sidecar). | | Auth already enforced at the ingress, gateway, or mesh | Set `thorasDashboard.auth.enabled: false`. See [Bring your own auth](/guides/dashboard-auth#bring-your-own-auth). | | `featureFlags.enableSimpleAuthSecret` in `values.yaml` | Rename to `apiClientSecret.enabled`. | | Deployed with Argo CD, Flux, or `helm template` | During the upgrade, keep `jsonPointers: [/data]` `ignoreDifferences` entries on the `api-client-secret` and `thoras-timescale-password` Secrets so Argo doesn't overwrite them before config-controller migrates their values. Once the pods are healthy, set `featureFlags.enableLegacySecretSeeding: false` and remove those entries. See [ArgoCD](/guides/argo-cd). | If you use the chart's legacy secret seeding (the 4.x default), complete the upgrade with `featureFlags.enableLegacySecretSeeding` left at its default of `true` so the config-controller can migrate your existing credentials. Once all pods are healthy, set it to `false`. # Support and Deprecation Policy Source: https://docs.thoras.ai/policies/support-policy Thoras support and deprecation policy: supported versions, compatibility commitments, Kubernetes version support, and breaking change notification. This policy describes how Thoras supports customers, communicates changes, and maintains stability across releases. The model is designed so that staying up to date is safe and straightforward: every release contains the latest fixes, upgrades within a major version do not require migration work, and any change that would require migration is communicated well in advance. ## Supported versions Thoras ships continuously. Bug fixes and security patches are included in every release, and customers receive them by upgrading to the latest version. Because Thoras maintains strict compatibility within a major version (described below), upgrades within a major are designed to be safe and routine: no migration work, no breaking changes, no surprises. Previous release branches are not maintained. ## Compatibility within a major version Within a major version, Thoras does not introduce breaking changes to: * `AIScaleTarget`, `ClusterAIScaleTemplate`, and other documented Custom Resource Definitions * Helm chart values (no renames or removals without the deprecation process described below) Behavior improvements that do not alter the interfaces listed above (forecasting accuracy, internal defaults, performance) may ship in any minor or patch release. This compatibility commitment is the foundation of the support model: because nothing breaks between releases within a major version, customers can confidently apply the latest release whenever they need a fix. ## APIs Thoras APIs are internal. They are not part of the public product surface, are not covered by this policy, and may change in any release without prior notice. Customers who require a stable API should contact their Thoras Product Adoption Engineer. Any API stability commitment requires a separate written agreement and is not implied by this policy. ## Breaking changes Breaking changes to the interfaces listed under "Compatibility within a major version" are reserved for major version releases. Before a major release, Thoras will provide: * At least 90 days written notice via release notes, the documentation site, and email to listed customer contacts * A migration guide The 90-day notice period gives customers time to plan and execute the migration. When the new major reaches general availability, support consolidates on the new version. For migrations that need additional time or coordination, customers should engage their Product Adoption Engineer; Thoras works directly with customers to make these transitions successful. ## Kubernetes version support Thoras supports the five most recent upstream Kubernetes minor versions. When a new upstream minor reaches general availability, the oldest supported version is dropped at the next Thoras release. The current list of supported versions is published in each Thoras release notes. Older versions may continue to work but are not part of the Thoras test matrix and compatibility is not guaranteed. ## Deprecations Any field or behavior marked deprecated within the interfaces listed under "Compatibility within a major version" will continue to function for at least 90 days from the deprecation announcement before removal. Deprecations are listed in release notes at the time of announcement and tracked at [docs.thoras.ai/deprecations](/deprecations), giving customers lead time to adjust. ## Out of scope * Alpha and beta features carry no compatibility or deprecation guarantee. They may change or be removed at any time. * Behaviors not described in the public documentation are not covered by this policy. ## Questions For questions about this policy or about a specific upgrade or migration, email [support@thoras.ai](mailto:support@thoras.ai) or contact your Thoras Product Adoption Engineer. # AIScaleTarget Source: https://docs.thoras.ai/reference/ast-definition The specification describes the `AIScaleTarget` Custom Resource that defines how Thoras scales workloads. The following is a sample `AIScaleTarget` definition: ```yaml ast.yaml theme={null} apiVersion: thoras.ai/v1 kind: AIScaleTarget metadata: name: "{{ YOUR_AST_NAME }}" namespace: "{{ YOUR_NAMESPACE }}" spec: scaleTargetRef: kind: Deployment name: "{{ YOUR_AST_NAME }}" model: forecast_blocks: 4h forecast_buffer_percentage: 0% forecast_interval: 1h mode: balanced horizontal: mode: recommendation scaling_behavior: scale_up: type: percent percent: 50 scale_down: type: percent percent: 50 vertical: containers: - name: "{{ CONTAINER_NAME }}" cpu: lowerbound: 20m upperbound: 1 memory: lowerbound: 50Mi upperbound: 2G mode: recommendation scaling_behavior: scale_up: type: percent percent: 50 scale_down: type: percent percent: 50 update_policy: update_mode: in_place_or_recreate ``` ## `metadata` ```yaml ast.yaml theme={null} metadata: name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} ``` It's recommended that `metadata.name` matches the name of the workload being scaled. ### Keeping history when an AST is recreated Requires chart version `5.6.0` Deleting an AIScaleTarget and creating it again, for example during a migration or a reinstall, gives it a new identity, and it starts with empty history. To keep the history of the AIScaleTarget it replaces, add the `thoras.ai/inherit-history` annotation when you create it: ```yaml ast.yaml theme={null} metadata: name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} annotations: thoras.ai/inherit-history: "true" ``` The new AIScaleTarget then reads the history of every earlier AIScaleTarget with the same namespace and name: its metric history and charts, forecast performance history, and cost history. The earlier data isn't copied or changed. Thoras only links it to the new AIScaleTarget. Current state still starts fresh: the latest forecast signals, current metrics, and readiness for autonomous mode come from the new AIScaleTarget's own data. A few rules apply: * **Set it at creation.** The annotation only takes effect on an AIScaleTarget that is less than 10 minutes old. On an older AIScaleTarget, Thoras reports `skipped-not-new` and inherits nothing. * **Only retained history is inherited.** A deleted AIScaleTarget's data is kept until it ages out under normal retention. Detailed metrics and forecast data are kept for 30 days, so only what's left of them comes back. Daily usage and cost history is kept longer. * **Undo is permanent.** Setting the annotation to `"false"` removes the link to the earlier history, which stays untouched. Setting it back to `"true"` afterwards doesn't restore it. * **Templates don't set it.** AIScaleTargets created by a [ClusterAIScaleTemplate](/reference/cluster-aiscale-template) don't carry the annotation, so they start with empty history. Thoras reports progress in the `thoras.ai/inherit-history-status` annotation. Its value starts with one of the statuses below, followed by the AIScaleTarget's UID: ```sh theme={null} kubectl get ast {{YOUR_AST_NAME}} -n {{YOUR_NAMESPACE}} \ -o jsonpath='{.metadata.annotations.thoras\.ai/inherit-history-status}' ``` | Status | Meaning | | - | - | | `queued` | Thoras is linking the earlier history. | | `done` | The earlier history is linked. `inherited=` is the number of earlier ASTs. | | `nothing-to-inherit` | No earlier AIScaleTarget with this name has retained data. | | `skipped-not-new` | The annotation was added after the first 10 minutes, so nothing was inherited. | | `removed` | The annotation was set to `"false"` and the link was removed. | | `failed` | Linking failed after several retries. The AIScaleTarget works with its own history. | ## `scaleTargetRef` ```yaml ast.yaml theme={null} scaleTargetRef: kind: Deployment name: {{YOUR_AST_NAME}} namespace: {{YOUR_NAMESPACE}} apiVersion: apps/v1 ``` `scaleTargetRef` is the reference to a specific workload that Thoras will scale up or down. The workload must reside in the same namespace as the `AIScaleTarget`. `apiVersion` specifies the API group and version that defines the resource, while `kind` identifies the type of resource (e.g., `Deployment`, `StatefulSet`, `Rollout`, etc). **Note:** `scaleTargetRef` and `selector` are mutually exclusive. Use `scaleTargetRef` to target a single workload by name, or use `selector` to target multiple workloads by label. ## `selector` ```yaml ast.yaml theme={null} selector: matchLabels: app: my-service environment: production ``` `selector` allows you to target multiple workloads by [pod labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/) instead of specifying a single workload by name. This is useful when you want to apply the same scaling policy to multiple workloads that share common labels. **Important constraints:** * `selector` and `scaleTargetRef` are mutually exclusive, you must use one or the other, not both. * When using `selector`, horizontal scaling is **not supported**. You must leave `spec.horizontal` empty or omit it entirely. * Pods must be managed by a Deployment, StatefulSet, or Argo Rollout. Standalone pods or other controller types are not supported. **How it works:** When a `selector` is defined, Thoras identifies all pod controllers (Deployments, StatefulSets, Rollouts) in the same namespace whose pods match the label selector. In `autonomous` mode: * Thoras restarts each matching pod controller one by one when a resource request suggestion comes in from the forecaster. * If `update_policy.update_mode` is set to `in_place` or `in_place_or_recreate`, pods are resized in place without restarts (when possible). ## `model` ```yaml ast.yaml theme={null} model: forecast_blocks: 4h forecast_buffer_percentage: 0% forecast_interval: 1h ``` ## `model.forecast_blocks` The window of future time each forecast covers. Thoras predicts the **peak** (maximum) load over that window and recommends resources or replicas for that peak, not for the average or the load at the end of the window. For example, with `forecast_blocks: 4h` and `forecast_interval: 1h`, every hour Thoras predicts the highest load expected in the next 4 hours and sizes the workload for it. The recommendation only drops once the predicted peak is more than 4 hours away. **Choosing a value:** * **Shorter windows** track demand more closely, so the workload scales down sooner after a peak passes. Lowest cost, least headroom. * **Longer windows** hold capacity for any peak that falls inside the window. More headroom, more overprovisioning between peaks. **Cost of changing it:** a longer window adds no meaningful load to Thoras. Changing the value retrains the workload's model once, and a model needs at least 4x `forecast_blocks` of metric history to train (e.g. `24h` needs 4 days). Until enough history exists, Thoras sizes for the peak of observed data. **Important:** `forecast_blocks` must be greater than or equal to `forecast_interval`. For example, if forecasts run every `1h`, `forecast_blocks` must be at least `1h`, otherwise there would be stretches of time no forecast covers. You will need to specify either the "m" (for minutes) or "h" (for hours) unit for the value of [model.forecast\_blocks](#model-forecast-blocks). ## `model.forecast_buffer_percentage` Defines an additional buffer applied on top of the forecasted resource usage to reduce the risk of under-provisioning. For example, if the forecasted CPU usage is 500m and `forecast_buffer_percentage` is set to 10%, the final recommended value will be 550m. Setting this to 0% means no buffer will be added. ## `model.forecast_interval` Controls how frequently Thoras generates forecasts and evaluates scaling. Accepts a plain duration string (e.g., `"15m"`, `"1h"`). Shorter intervals can lead to more accurate forecasts but may result in more frequent scaling. `forecast_interval` controls how often forecasts are made, while [model.forecast\_blocks](#model-forecast-blocks) defines the time window each forecast covers. ## `model.mode` Defines the optimization strategy that controls how tightly forecast recommendations track expected usage. Thoras modes span a spectrum from efficiency-focused to assurance-focused: `undershoot` → `cost_optimized` → `balanced` → `assurance_optimized` → `overshoot` Available modes: * `undershoot` - Most aggressive strategy, allowing recommendations to run below usage more often * `cost_optimized` - Tighter recommendations with increased efficiency focus * `balanced` (default) - Balanced reliability and efficiency for general production workloads * `assurance_optimized` - More conservative recommendations that track above typical usage patterns * `overshoot` - Most conservative strategy, maintaining additional headroom based on historical usage * `percentile` - Deterministic history-based forecast set at a specific percentile of observed usage. Requires `percentile_value` (0–100). An integer or quoted decimal is accepted (e.g. `75` or `"99.9"`). If not specified, the default is `balanced`. **Visit [Choosing a Forecast Optimization Strategy](/guides/forecast-optimization-strategies) for detailed guidance on selecting the right mode for your workload.** ## `model.metrics` ```yaml ast.yaml theme={null} model: mode: balanced metrics: cpu: mode: undershoot memory: mode: balanced forecast_buffer_percentage: 10% ``` Per-metric overrides allow you to configure different forecast settings for CPU and memory independently, overriding the global `model.mode` and `model.forecast_buffer_percentage` settings. **Common use cases:** * Apply aggressive cost optimization to CPU while maintaining conservative memory forecasts * Add extra buffer to memory forecasts to prevent OOM errors while keeping CPU forecasts tight * Test different optimization strategies per resource type **Configuration:** Each metric override supports: * `mode` - Override the global forecast mode for this metric. Same values as the top-level `model.mode`: `balanced`, `cost_optimized`, `undershoot`, `overshoot`, `assurance_optimized`, or `percentile`. * `forecast_buffer_percentage` - Override the global buffer percentage for this metric **Example: Mixed strategy** ```yaml ast.yaml theme={null} model: mode: balanced # Global default forecast_buffer_percentage: 0% metrics: cpu: mode: undershoot # More aggressive for CPU memory: mode: balanced forecast_buffer_percentage: 10% # Extra buffer for memory ``` In this configuration: * CPU uses `undershoot` mode (ignoring global `balanced`) * Memory uses `balanced` mode with 10% buffer (ignoring global 0%) If a metric is not specified in `model.metrics`, it inherits the global `model.mode` and `model.forecast_buffer_percentage` settings. ## `model.min_lookback_to_scale` Minimum lookback window required before autonomous scaling is enabled. Autonomous scaling will only occur if historical data spans at least this duration. **Default:** `24h` (inherited from helm chart value `thorasForecast.minLookbackToScale` if not specified) **Format:** Duration string (e.g., `"24h"`, `"7h30m"`). Cannot exceed metrics retention window (typically 14 days). **Example:** ```yaml ast.yaml theme={null} model: min_lookback_to_scale: "24h" # Override default: require 24 hours before autonomous scaling ``` This setting ensures sufficient historical data exists before allowing autonomous scaling decisions. Only applies when `mode` is set to `autonomous` - has no effect in recommendation mode. ## `horizontal` ```yaml ast.yaml theme={null} horizontal: mode: recommendation minReplicas: 2 maxReplicas: 20 ``` Thoras horizontal scaling adjusts the number of replicas based on forecasted or observed workload demands. The `mode` field controls whether Thoras provides recommendations or actively scales your workload: In `autonomous` mode: * Horizontally-scaled workloads **must** have CPU and/or memory requests defined in the pod spec if you plan to scale on `averageUtilization`. * **The target deployment of your `AIScaleTarget` must have a single existing [Horizontal Pod Autoscaler (HPA)](https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/).** > **Note:** Only one scaling direction (horizontal or vertical) can be in > `autonomous` mode at a time. See > [Understanding Vertical and Horizontal Scaling Modes](/guides/configuring-asts#understanding-vertical-and-horizontal-scaling-modes) > for additional details. In `recommendation` mode : * System does **not** require an existing HPA. Thoras will assume an 80% utilization target for CPU and memory when generating scaling suggestions. Thoras does **not** replace your HPA, it works alongside it by feeding forecasted metrics into your existing HPA configuration. You can customize behavior by: * Opting in additional metrics using `spec.horizontal.additional_metrics` * Opting out of default metrics using `spec.horizontal.exclude_metrics` This setup gives you full control over how scaling decisions are made while benefiting from Thoras' predictive intelligence. To enable horizontal scaling recommendations, set `spec.horizontal.mode` to `recommendation`. It is recommended to start in `recommendation` mode to allow the model time to learn workload patterns and validate scaling suggestions. Once the recommendations align with your performance expectations, you can switch to `autonomous` mode for automated scaling. ## `horizontal.minReplicas` The lower bound for the number of replicas Thoras will scale down to. When set, this value takes precedence over the `minReplicas` configured on the target HPA. If not set, Thoras falls back to the HPA's `minReplicas` value. **Note:** `minReplicas` must not be greater than `maxReplicas` when both are set , the CRD will reject the configuration if this constraint is violated. ## `horizontal.maxReplicas` The upper bound for the number of replicas Thoras will scale up to. When set, this value takes precedence over the `maxReplicas` configured on the target HPA. If not set, Thoras falls back to the HPA's `maxReplicas` value. **Note:** `maxReplicas` must not be less than `minReplicas` when both are set , the CRD will reject the configuration if this constraint is violated. **Example: Constraining replicas independently of HPA** ```yaml ast.yaml theme={null} horizontal: mode: autonomous minReplicas: 2 maxReplicas: 20 ``` This gives teams fine-grained control over Thoras scaling behavior independent of HPA configuration. The HPA bounds remain in place and continue to govern Kubernetes' own scaling, but Thoras will only apply replica counts within the AST-defined range. **Visit [Predictive Horizontal Pod Autoscaling with Thoras Guide](/guides/hpa) for more info.** ## `horizontal.multipleOf` Constrains autonomous replica scaling to exact multiples of this value. When set, Thoras rounds any desired replica count **up** to the nearest multiple before applying it. Must be a positive integer. The CRD rejects configurations where `multipleOf` exceeds `maxReplicas`. **Example: StatefulSet sharded across 8 partitions** ```yaml ast.yaml theme={null} horizontal: mode: autonomous maxReplicas: 24 multipleOf: 8 ``` Thoras will only set replica counts of 8, 16, or 24. **Visit [Constraining Replicas to Fixed Multiples](/guides/hpa#constraining-replicas-to-fixed-multiples) for more info.** ## `horizontal.allowedReplicas` Constrains autonomous replica scaling to an explicit set of valid replica counts. When set, Thoras rounds any desired count **up** to the smallest value in the set that meets or exceeds it, and never scales beyond the largest value. Must be a non-empty list of unique positive integers (up to 64 values). Every value must fall within `minReplicas` and `maxReplicas` when those are set, and `allowedReplicas` cannot be combined with `multipleOf`. The CRD rejects configurations that violate these constraints. **Example: a topic sharded across divisors of its partition count** ```yaml ast.yaml theme={null} horizontal: mode: autonomous minReplicas: 4 maxReplicas: 20 allowedReplicas: [4, 6, 8, 12, 16] ``` Thoras will only set replica counts from this set. For example, a forecast of 10 scales to 12. **Visit [Constraining Replicas to a Fixed Set](/guides/hpa#constraining-replicas-to-a-fixed-set) for more info.** ## `vertical` ```yaml ast.yaml theme={null} vertical: containers: - name: {{CONTAINER_NAME}} cpu: lowerbound: 20m # Mandatory upperbound: 1 # Optional memory: lowerbound: 50Mi # Mandatory upperbound: 2G # Optional mode: recommendation update_policy: update_mode: in_place_or_recreate ``` The Thoras vertical scaler adjusts container-level CPU and memory resource requests based on forecasted utilization. The `mode` field controls whether Thoras provides recommendations or actively scales your workload: To define a vertical scaling policy, you'll want to set the following fields: * `vertical.mode: recommendation` - we recommend running Thoras in `recommendation` mode for at least a day for the model to train before enabling `autonomous`. * `vertical.containers[0].memory.lowerbound` is always required and `vertical.containers[0].memory.upperbound` is optional if you had a preference for a memory floor and ceiling for your target workload. In `autonomous` mode, changing these bounds re-enforces them immediately on live pods that fall outside the new range, according to `update_policy`. With `update_mode: initial`, bounds apply only when pods are next created. * `vertical.containers[].RESOURCE.limit` allows Thoras to modify the container's limit along with the request for this resource and is optional. If unset, Thoras will *not* modify limits for this resource. * `vertical.containers[].RESOURCE.limit.ratio` the ratio used to keep the limit inline with the suggested request for this resource. E.g. for a ratio `2.5` Thoras will make the limit `2.5G` if the suggestion for the request was for `1G`. ### Limit Enforcement Thoras respects resource limits to ensure safe right-sizing operations: * **AIScaleTarget `upperbound`**: Thoras won't right-size a pod above the configured `upperbound` during steady-state operation. The one exception is [OOM remediation](/guides/oom-remediation), an opt-in emergency response that intentionally bypasses `memory.upperbound` to recover a workload from an out-of-memory kill loop; the bound is re-applied once its stabilization window expires. * **Bound changes in autonomous mode**: If you update `lowerbound` or `upperbound` and live requests are outside the new range, Thoras applies the new bounds right away per `update_policy` (see [Predictive Vertical Pod Rightsizing](/guides/vertical-pod-rightsizing#vertical-breakdown)). * **Pod spec `limit`** is also enforced, Thoras will never right-size a pod above its defined limit * **Combined enforcement**: When both `upperbound` and pod `limit` are set, Thoras uses the lower value as the maximum for right-sizing > **Note:** Only one scaling direction (horizontal or vertical) can be in > `autonomous` mode at a time. See > [Understanding Vertical and Horizontal Scaling Modes](/guides/configuring-asts#understanding-vertical-and-horizontal-scaling-modes) > for details on how vertical and horizontal modes interact. **Visit [Predictive Vertical Pod Rightsizing Guide](/guides/vertical-pod-rightsizing) for more info.** ### Container Name Wildcard Matching The `vertical.containers[].name` field supports wildcard patterns using `*` to match multiple containers with a single configuration. This is useful when you have multiple containers with similar naming patterns or when you want to apply the same resource bounds to all containers. **Important:** Container configurations are matched in order from top to bottom. The first matching pattern is used. Place more specific patterns before generic ones. **Matching Rules:** * **Exact match**: Pattern matches the container name exactly * **Wildcard `*`**: Matches any sequence of characters (including empty string) * **Prefix match**: `app-*` matches any container starting with `app-` * **Suffix match**: `*-sidecar` matches any container ending with `-sidecar` * **Contains match**: `*cache*` matches any container containing `cache` * **Multiple wildcards**: `app-*-prod-*` matches containers with `app-` prefix and `-prod-` in the middle **Important:** If a pattern doesn't start with `*`, it must match from the beginning. If it doesn't end with `*`, it must match to the end. **Examples:** ```yaml ast.yaml theme={null} vertical: containers: # Specific patterns first - these will match before the wildcard # Match containers starting with "app-" - name: "app-*" cpu: lowerbound: 100m upperbound: 2 memory: lowerbound: 128Mi upperbound: 4G # Match containers ending with "-sidecar" - name: "*-sidecar" cpu: lowerbound: 10m upperbound: 500m memory: lowerbound: 32Mi upperbound: 512Mi # Match containers containing "cache" - name: "*cache*" cpu: lowerbound: 50m upperbound: 1 memory: lowerbound: 256Mi upperbound: 8G # Match specific pattern with multiple wildcards - name: "app-*-prod-*" cpu: lowerbound: 200m upperbound: 4 memory: lowerbound: 512Mi upperbound: 16G # Generic wildcard last - catches any remaining containers # This will only match containers that didn't match patterns above - name: "*" cpu: lowerbound: 20m upperbound: 1 memory: lowerbound: 50Mi upperbound: 2G ``` **Pattern Matching Examples:** | Pattern | Container Name | Matches | Reason | | - | - | - | - | | `*` | `any-container` | ✅ | Wildcard matches everything | | `app-*` | `app-frontend` | ✅ | Starts with `app-` | | `app-*` | `my-app-frontend` | ❌ | Doesn't start with `app-` | | `*-sidecar` | `logging-sidecar` | ✅ | Ends with `-sidecar` | | `*-sidecar` | `sidecar-proxy` | ❌ | Doesn't end with `-sidecar` | | `*cache*` | `redis-cache-primary` | ✅ | Contains `cache` | | `*cache*` | `redis-primary` | ❌ | Doesn't contain `cache` | | `app-*-prod` | `app-frontend-prod` | ✅ | Matches pattern | | `app-*-prod` | `app-frontend-prod-v2` | ❌ | Has extra content after `prod` | ### `update_policy` ```yaml theme={null} vertical: update_policy: update_mode: in_place_or_recreate recreate_resources: ["memory"] ``` The `update_policy` field controls pod update behavior during vertical scaling operations. **Prerequisites for in-place updates:** * Kubernetes 1.33+ **Configuration:** * `update_mode` (string): Determines how pod updates are applied: * `recreate` - Pods are recreated when resource changes are applied * `in_place` - Changes applied without pod recreation (requires Kubernetes 1.33+) * `in_place_or_recreate` - Attempts in-place updates with fallback to recreation (recommended) * `initial` - Recommendations apply only during pod creation * `recreate_resources` (optional array): Specifies which resources (`memory`, `cpu`) trigger pod recreation when using recreate or fallback modes **How it works:** 1. When `update_mode` is `in_place` or `in_place_or_recreate`, Thoras attempts to resize pods in place using the Kubernetes resize API. Resizes are applied in batches rather than to the whole workload at once — the batch size and cadence are controlled by [`disruption`](#disruption) 2. When `update_mode` is `in_place_or_recreate` and the pod's [QoS class](https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/) would change or the resize operation is not supported, Thoras falls back to evicting the pod 3. Evicted pods are rolled out gradually rather than all at once, minimizing service disruption. The eviction batch size and cadence are also controlled by [`disruption`](#disruption) For details on what causes a resize to stall and how each mode responds, see [In-Place Resize Behavior](/guides/in-place-resize). **Benefits:** * Reduced disruption: Pods are not recreated unless necessary when using in-place updates * Faster scaling: Resource adjustments take effect immediately * Better node utilization: Enables more efficient bin-packing and cost savings, especially when combined with node autoscaling tools like Karpenter * Graceful fallback: Automatic eviction when in-place resizing is not possible * Fine-grained control: Specify which resources trigger pod recreation ### `oom_remediation` ```yaml theme={null} vertical: mode: autonomous oom_remediation: enabled: true stabilization_window: 2h ``` Enables automatic memory adjustment when OOM kills are detected. Only applies when `vertical.mode` is `autonomous`. **Fields:** | Field | Type | Required | Default | Description | | - | - | - | - | - | | `enabled` | bool | Yes | `false` | Activates OOM remediation for this workload. | | `stabilization_window` | duration | No | `max(forecast_interval, 1h)` | How long the adjusted memory is held as a floor after an OOM. Forecasts can raise memory above the floor but cannot lower it until the window expires. Resets on each new OOM adjustment. | **Visit [OOM Remediation Guide](/guides/oom-remediation) for detailed configuration guidance.** ### `disruption` ```yaml ast.yaml theme={null} vertical: disruption: interval: 60s max_disrupted: "20%" ``` `disruption` controls how aggressively Thoras applies pod changes (in-place resize or eviction) when applying vertical recommendations. Whether Thoras resizes a pod in place or has to recreate (evict) it to apply a recommendation, these settings pace those changes in batches so the workload is not disrupted all at once. This setting applies only when vertical scaling is in `autonomous` mode. It has no effect in `recommendation` mode. It governs both in-place resizes and evictions (see [`update_policy`](#update_policy)) — an entire workload is never resized in one shot. If `disruption` is omitted, Thoras applies the defaults below. **Configuration:** * `interval` (string, default `60s`): The minimum time Thoras waits after a batch of pod changes (resize or eviction) before starting the next batch. Must be a positive duration. * `max_disrupted` (integer or percentage, default `20%`): The maximum number of pods that may be disrupted at once across the workload, counting both pods resized in place and pods evicted in the batch. Accepts an absolute integer (e.g. `2`) or a percentage of the workload's desired replicas (e.g. `"25%"`). Must be a positive integer or a percentage between 1% and 100%. **How it works:** * Thoras applies changes to stale pods in batches — resizing in place where possible, evicting otherwise. After issuing a batch it waits `interval` before issuing the next one, so changes roll out gradually instead of all at once. * A pod counts as "disrupted" if it is terminating or its `Ready` condition is not `True`. Each batch is sized against the remaining headroom (`max_disrupted` minus the pods already disrupted), so batches that haven't finished rolling shrink the next batch. * Unlike a `PodDisruptionBudget`'s `maxUnavailable`, this cap is enforced by Thoras itself: pods that are unavailable for reasons unrelated to Thoras (e.g. failing readiness probes) still consume the budget. * If the Kubernetes API rate-limits Thoras's resize requests, Thoras stops the current batch and resumes the remaining pods on a later run rather than continuing to hammer the API. * Thoras still honors your `PodDisruptionBudget`s. If an eviction is blocked by a PDB, Thoras defers the remaining evictions to a later run rather than forcing them. ## `additional_configuration` ### `scaling_behavior` Controls scaling thresholds to prevent unnecessary scaling actions. Thresholds can be configured independently for `scale_up` and `scale_down`. Both **percentage** and **absolute** thresholds can be used for horizontal and vertical scaling. If `scaling_behavior` is not configured, defaults are applied automatically based on your `model.mode`. Explicit configuration here overrides those defaults. The default mode is `balanced`, so if no `model.mode` is set, the 5%/5% thresholds apply. **Default thresholds by model mode:** | Mode | Scale up threshold | Scale down threshold | | - | - | - | | `undershoot` | 20% | 5% | | `cost_optimized` | 10% | 5% | | `balanced` | 5% | 5% | | `assurance_optimized` | 5% | 10% | | `overshoot` | 5% | 20% | Cost-leaning modes (`undershoot`, `cost_optimized`) have a higher scale-up threshold and a lower scale-down threshold: they scale down readily and require a larger change before scaling up. Assurance-leaning modes (`assurance_optimized`, `overshoot`) do the opposite. See [`model.mode`](#model-mode) for mode details. **Percentage Thresholds** - Trigger scaling when change exceeds a percentage of current usage (works for both horizontal and vertical): ```yaml theme={null} horizontal: scaling_behavior: scale_up: type: percent percent: 20 # Scale up when increase is at least 20% scale_down: type: percent percent: 15 vertical: scaling_behavior: scale_up: type: percent percent: 20 # Scale up when increase is at least 20% scale_down: type: percent percent: 15 ``` **Absolute Thresholds** - Trigger scaling when change exceeds fixed amounts (works for both horizontal and vertical): For horizontal scaling, use `pods`: ```yaml theme={null} horizontal: scaling_behavior: scale_up: type: absolute absolute: pods: "5" # Scale up only if adding at least 5 pods scale_down: type: absolute absolute: pods: "3" # Scale down only if removing at least 3 pods ``` For vertical scaling, use `memory` and `cpu`: ```yaml theme={null} vertical: scaling_behavior: scale_up: type: absolute absolute: memory: "100Mi" # Scale up only if memory increase is at least 100Mi cpu: "500m" # Scale up only if CPU increase is at least 500m scale_down: type: absolute absolute: memory: "50Mi" # Scale down only if memory decrease is at least 50Mi cpu: "250m" # Scale down only if CPU decrease is at least 250m ``` **Best Practices:** * Use absolute thresholds for small workloads (percentage changes can be misleading) * Use percentage thresholds for large workloads (scales naturally with deployment size) * Set asymmetric thresholds (conservative scale-up, aggressive scale-down) * If you are unsure where to start, rely on the mode defaults and only override if you observe unwanted scaling behavior ### `additional_metrics` and `exclude_metrics` ```yaml ast.yaml theme={null} horizontal: mode: recommendation additional_metrics: - external: metric: name: {{EXTERNAL_METRIC_NAME}} selector: matchLabels: app: {{APP_NAME}} target: averageValue: "1" type: AverageValue type: External exclude_metrics: - name: cpu type: Resource ``` When you enable horizontal scaling in Thoras, it defaults to using the same metrics as your HPA (Horizontal Pod Autoscaler) to guide predictive scaling, whether they're resource, external or custom metrics. However, there may be cases where you want Thoras to: * **Exclude certain metrics** (e.g., CPU) from its predictions, even if they are used by the HPA. * **Add new metrics** for prediction that are not part of the HPA. ### Example 1: **Exclude CPU from Predictive Scaling** This configuration tells Thoras **not to use CPU** for predictive scaling, even if it's included in the HPA: ```yaml ast.yaml theme={null} spec: horizontal: exclude_metrics: - name: cpu type: Resource ``` ### Example 2: **Add Custom Metric for Predictive Scaling** In this example, custom metric is configured to **predictively scale on CPU** by explicitly including it in `additional_metrics`. Thoras will predictively scale on CPU even though it is not defined in the HPA. ```yaml ast.yaml theme={null} spec: horizontal: additional_metrics: - external: metric: name: {{EXTERNAL_METRIC_NAME}} selector: matchLabels: app: {{APP_NAME}} target: averageValue: "1" type: AverageValue type: External ``` # ClusterAIScaleTemplate Source: https://docs.thoras.ai/reference/cluster-aiscale-template ClusterAIScaleTemplate is a cluster-scoped custom resource that automatically creates and manages AIScaleTargets for matching workloads across multiple namespaces. This feature eliminates the need to manually create individual AIScaleTargets for each workload, enabling fleet-wide scaling policies. ## Overview ClusterAIScaleTemplate watches for Deployments, StatefulSets, and Argo Rollouts that match a label selector, and automatically creates corresponding [AIScaleTargets](/reference/ast-definition) with a predefined configuration. When workloads are added, updated, or removed, Thoras automatically manages the lifecycle of the associated AIScaleTargets. ## Use Cases * Apply AI-powered scaling to all workloads with a specific label (e.g., `thoras.ai/enabled: "true"`) * Standardize scaling policies across multiple microservices * Automatically enable AI scaling for new workloads as they're deployed * Manage scaling configuration for entire application tiers or environments ## Basic Example ```yaml theme={null} apiVersion: thoras.ai/v1 kind: ClusterAIScaleTemplate metadata: name: production-workloads spec: # Match workloads with specific labels selector: matchLabels: env: production thoras.ai/enabled: "true" # Template to apply to matching workloads template: model: forecast_interval: 1h forecast_blocks: 4h forecast_buffer_percentage: 10% mode: balanced vertical: mode: auto update_policy: update_mode: in_place_or_recreate containers: - name: "*" cpu: lowerbound: "10m" memory: lowerbound: 0 ``` This template will automatically create AIScaleTargets for all Deployments, StatefulSets, and Argo Rollouts in any namespace that have the labels `env: production` and `thoras.ai/enabled: "true"`. ## Specification ### ClusterAIScaleTemplateSpec | Field | Type | Required | Description | | - | - | - | - | | `selector` | `metav1.LabelSelector` | Yes | Matches workloads to apply this template to. Supports `matchLabels` and `matchExpressions`. | | `template` | `AIScaleTemplateSpec` | Yes | Defines the AIScaleTarget configuration to apply. | | `policy` | `TemplatePolicy` | No | Controls template application behavior. | ### AIScaleTemplateSpec The template spec accepts the full [AIScaleTarget spec](/reference/ast-definition), minus the `scaleTargetRef` (which is automatically populated based on the matched workload). Any field you can set on an AIScaleTarget can be set on the template, including `horizontal.multipleOf`, `horizontal.allowedReplicas`, `vertical.disruption`, `vertical.update_policy`, `vertical.oom_remediation`, and per-metric mode overrides. Top-level fields: | Field | Type | Required | Description | | - | - | - | - | | `model` | `ModelSpec` | Yes | Model configuration (mode, [forecast settings](/reference/forecasting), buffer percentage). | | `vertical` | `VerticalSpec` | No | [Vertical scaling](/guides/vertical-pod-rightsizing) configuration. | | `horizontal` | `HorizontalSpec` | No | [Horizontal scaling](/guides/hpa) configuration. | See the [AIScaleTarget reference](/reference/ast-definition) for the field-by-field detail of each sub-spec. ### TemplatePolicy | Field | Type | Required | Default | Description | | - | - | - | - | - | | `conflictResolution` | `string` | No | `skip` | How to handle conflicts with existing AIScaleTargets. Options: `skip`, `override`. | | `includedNamespaces` | `[]string` | No | - | Allowlist of namespaces to target. Mutually exclusive with `excludedNamespaces`. | | `excludedNamespaces` | `[]string` | No | - | Denylist of namespaces to skip. Mutually exclusive with `includedNamespaces`. | ## Behavior ### Automatic AIScaleTarget Creation When a workload (Deployment, StatefulSet, or Argo Rollout) matches the template's selector: 1. Thoras creates an AIScaleTarget with the name `-ast` 2. The AIScaleTarget is labeled with `thoras.ai/managed-by-template: ` 3. The AIScaleTarget has an owner reference to the workload (so it's deleted when the workload is deleted) 4. The `scaleTargetRef` is automatically populated with the workload's details ### Lifecycle Management * **Workload Created**: AIScaleTarget is automatically created * **Workload Updated**: If labels change and no longer match, the AIScaleTarget is deleted * **Workload Opted Out**: If the workload is annotated with `thoras.ai/skip-template: "true"`, the template stops managing it. Any existing AIScaleTarget is retained, not deleted; see [Opting a Workload Out](#opting-a-workload-out) * **Workload Deleted**: AIScaleTarget is automatically deleted via owner reference * **Template Updated**: All managed AIScaleTargets are updated to match the new template * **Template Deleted**: All managed AIScaleTargets are automatically deleted ### Conflict Resolution When reconciling a controller (e.g. Deployment, Rollout, etc.), Thoras checks whether an AIScaleTarget already exists that targets the controller via `scaleTargetRef` and applies the following `conflictResolution` policies: * **`skip` (default)**: The existing AIScaleTarget is left unchanged. Changes made to the template will not be reflected in the conflicting AIScaleTarget. * **`override`**: The template takes ownership of the conflicting AIScaleTarget. The template's configuration is applied to the AIScaleTarget. Note: Thoras checks the `thoras.ai/managed-by-template` label to determine ownership. Only AIScaleTargets created by that specific template will be updated or deleted by it. ### Opting a Workload Out A workload that matches a template's selector can still opt out of template management by setting an annotation on the workload itself (Deployment, StatefulSet, or Argo Rollout) — not on its pods: ```yaml theme={null} metadata: annotations: thoras.ai/skip-template: "true" ``` The behavior depends on whether an AIScaleTarget already exists for the workload: * **No AIScaleTarget exists yet**: none is created while the annotation is present. * **An AIScaleTarget already exists**: it is left in place. The template stops updating it, but its history, model state, and suggestions are retained. A retained AIScaleTarget is not paused — it keeps scaling using its last template-applied configuration. Opting out of a template stops the template from managing the AIScaleTarget; it does not stop autoscaling. See [Stopping Autonomous Scaling](/guides/stopping-autoscaling) if your goal is to halt scaling actions. Removing the annotation (or setting it to `"false"`) resumes template management on the next reconcile, updating any retained AIScaleTarget in place. The annotation value is parsed as a boolean: `"true"`, `"True"`, `"TRUE"`, and `"1"` opt the workload out. `"false"`, `"0"`, an absent annotation, and empty or unparseable values are all treated as not opted out. ### Namespace Filtering Use `policy.includedNamespaces` or `policy.excludedNamespaces` to control which namespaces the template targets. The two fields are mutually exclusive, set one or the other, not both. * **`includedNamespaces`** (allowlist): only the listed namespaces are in scope. All other namespaces are ignored. * **`excludedNamespaces`** (denylist): all namespaces are in scope except those listed. No namespaces are excluded by default. If you want to protect system namespaces, add them explicitly: ```yaml theme={null} policy: excludedNamespaces: - kube-node-lease - kube-public - kube-system ``` ## Advanced Examples ### Using matchExpressions ```yaml theme={null} apiVersion: thoras.ai/v1 kind: ClusterAIScaleTemplate metadata: name: microservices-tier spec: selector: matchExpressions: - key: tier operator: In values: - frontend - backend - key: thoras.ai/enabled operator: Exists policy: excludedNamespaces: - kube-system - default - monitoring template: model: forecast_blocks: 4h forecast_buffer_percentage: 0% forecast_interval: 1h mode: balanced horizontal: mode: autonomous ``` ### Restricting to Specific Namespaces Use `includedNamespaces` to target only the namespaces you want: ```yaml theme={null} apiVersion: thoras.ai/v1 kind: ClusterAIScaleTemplate metadata: name: production-only spec: selector: matchLabels: thoras.ai/enabled: "true" policy: includedNamespaces: - production - staging template: model: forecast_blocks: 4h forecast_buffer_percentage: 0% forecast_interval: 1h mode: balanced vertical: mode: auto containers: - name: "*" cpu: lowerbound: "10m" memory: lowerbound: 0 ``` ### Multiple Templates You can create multiple templates with different selectors for different workload categories: ```yaml theme={null} --- apiVersion: thoras.ai/v1 kind: ClusterAIScaleTemplate metadata: name: critical-services spec: selector: matchLabels: priority: critical template: model: forecast_blocks: 4h forecast_buffer_percentage: 20% forecast_interval: 1h mode: balanced vertical: mode: auto containers: - name: "*" cpu: lowerbound: "500m" memory: lowerbound: "1Gi" --- apiVersion: thoras.ai/v1 kind: ClusterAIScaleTemplate metadata: name: standard-services spec: selector: matchExpressions: - key: priority operator: NotIn values: - critical - key: thoras.ai/enabled operator: Exists template: model: forecast_blocks: 4h forecast_buffer_percentage: 0% forecast_interval: 1h mode: balanced vertical: mode: auto containers: - name: "*" cpu: lowerbound: "0" memory: lowerbound: "0" ``` ## Status and Monitoring ClusterAIScaleTemplate includes a status subresource with conditions to track reconciliation state: ```yaml theme={null} status: conditions: - type: Ready status: "True" reason: ReconciliationSucceeded message: "Successfully reconciled 15 namespaces" lastTransitionTime: "2024-01-15T10:30:00Z" observedGeneration: 2 ``` ### Condition Types | Type | Status | Reason | Description | | - | - | - | - | | Ready | True | ReconciliationSucceeded | Template successfully reconciled all namespaces | | Ready | False | ReconciliationFailed | Failed to reconcile one or more namespaces | | Ready | False | NamespaceListFailed | Failed to list namespaces | Check template status: ```bash theme={null} kubectl get clusteraiscaletemplate production-workloads -o yaml # or using short name kubectl get clusterast production-workloads -o yaml ``` View all managed AIScaleTargets: ```bash theme={null} kubectl get aiscaletarget -A -l thoras.ai/managed-by-template=production-workloads ``` ## Best Practices 1. **Use Specific Selectors**: Use precise label selectors to avoid unintended matches. Consider using a dedicated label like `thoras.ai/enabled: "true"` on workloads you want to scale. 2. **Test in Non-Production First**: Create a template in a development environment first to verify the selector matches the intended workloads. 3. **Monitor Template Status**: Regularly check the template's status conditions to ensure successful reconciliation. 4. **Namespace Filtering**: No namespaces are excluded by default. Use `policy.excludedNamespaces` to block system namespaces (e.g., `kube-system`, `kube-public`, `kube-node-lease`) and any namespaces where automated scaling could cause issues. Use `policy.includedNamespaces` when you want to restrict the template to a known set of namespaces. 5. **Template Naming**: Use descriptive names that indicate the purpose or scope (e.g., `production-frontend`, `staging-all-services`). 6. **Label Workloads Explicitly**: Rather than relying on existing labels, add a specific label to workloads you want managed by templates (e.g., `thoras.ai/template: "production-workloads"`). 7. **Avoid Overlapping Templates**: Ensure template selectors don't overlap in ways that would cause multiple templates to match the same workloads. ## Troubleshooting ### Template Not Creating AIScaleTargets 1. Check the template status: ```bash theme={null} kubectl get clusterast -o yaml ``` 2. Verify workloads match the selector: ```bash theme={null} kubectl get deployments -A --show-labels ``` 3. Check namespace filtering configuration: ```bash theme={null} kubectl get clusterast -o jsonpath='{.spec.policy.includedNamespaces}' kubectl get clusterast -o jsonpath='{.spec.policy.excludedNamespaces}' ``` 4. View operator logs: ```bash theme={null} kubectl logs -n thoras -l app=thoras-operator ``` ### Workload Matches the Selector But Has No AIScaleTarget If a workload matches the template's selector and namespace filtering, but no AIScaleTarget is created (or an existing one stops being updated), check for the opt-out annotation: ```bash theme={null} kubectl get deployment -n -o jsonpath='{.metadata.annotations.thoras\.ai/skip-template}' ``` If this returns `true`, the workload has opted out of template management. See [Opting a Workload Out](#opting-a-workload-out). Remove the annotation to resume management. ### AIScaleTarget Not Updating When Template Changes Ensure the AIScaleTarget has the `thoras.ai/managed-by-template` label: ```bash theme={null} kubectl get ast -n -o jsonpath='{.metadata.labels}' ``` If the label is missing or points to a different template, the AIScaleTarget was not created by this template and won't be updated by it. ### Deleting Template But AIScaleTargets Remain Check if the finalizer is stuck: ```bash theme={null} kubectl get clusterast -o jsonpath='{.metadata.finalizers}' ``` View operator logs for errors during cleanup: ```bash theme={null} kubectl logs -n thoras -l app=thoras-operator | grep "cleaning up managed AIScaleTargets" ``` ## Migration from Manual AIScaleTargets To migrate existing manually created AIScaleTargets to template management: 1. Create a ClusterAIScaleTemplate with the desired configuration 2. Add matching labels to your workloads 3. The template will skip existing AIScaleTargets (due to default `skip` conflict resolution) 4. Manually delete existing AIScaleTargets - they will be automatically recreated by the template 5. Verify new AIScaleTargets have the `thoras.ai/managed-by-template` label Alternatively, manually add the label to existing AIScaleTargets: ```bash theme={null} kubectl label ast -n thoras.ai/managed-by-template= ``` **Warning**: Only do this if you're certain the template configuration matches your existing AIScaleTarget configuration, as the template will overwrite the spec on the next reconciliation. ## Related Documentation * [AIScaleTarget Definition](/reference/ast-definition) - Complete reference for AIScaleTarget specifications * [Configuring AIScaleTargets](/guides/configuring-asts) - Step-by-step guide for creating AIScaleTargets * [Forecasting](/reference/forecasting) - Understanding how Thoras forecasts resource needs * [Predictive Horizontal Pod Autoscaling](/guides/hpa) - Guide to horizontal scaling configuration * [Predictive Vertical Pod Rightsizing](/guides/vertical-pod-rightsizing) - Guide to vertical scaling configuration * [Enabling Autonomous Mode](/guides/enabling-autonomous-mode) - Prerequisites for autonomous scaling * [Pod Annotations](/reference/pod-annotations) - The `thoras.ai/managed` family of annotations the webhook writes to mutated pods, distinct from the `thoras.ai/managed-by-template` label on this page * [Stopping Autonomous Scaling](/guides/stopping-autoscaling) - Options for halting Thoras autoscaling, from cluster-wide to per-workload # Forecasting Source: https://docs.thoras.ai/reference/forecasting Thoras forecasts future resource usage and scaling needs based on historical patterns. This reference documents how forecasting works and the key concepts involved. ## How Forecasting Works Thoras continuously collects workload metrics and uses those timeseries metrics to generate forecasts. When a forecast is generated, Thoras predicts future resource usage within the forecast block. When a target is in autonomous mode and the delta between forecasted and current usage exceeds configured thresholds, the target is proactively scaled to the predicted maximum within the forecast block, plus a small buffer. Thoras generates forecasts for two scaling dimensions: * **Horizontal** - Predicts pod replica count based on forecasted CPU, memory, and custom metrics. See [Predictive Horizontal Pod Autoscaling](/guides/hpa) for details. * **Vertical** - Predicts container resource requests based on historical consumption patterns. See [Vertical Pod Rightsizing](/guides/vertical-pod-rightsizing) for details. ## Forecast Configuration Forecasting behavior is configured in the `model` section of an `AIScaleTarget`. See the [AIScaleTarget Definition](/reference/ast-definition#model) for configuration options including: * [`forecast_blocks`](/reference/ast-definition#modelforecast_blocks) - the time horizon for each forecast; Thoras sizes for the predicted peak within it * [`forecast_buffer_percentage`](/reference/ast-definition#modelforecast_buffer_percentage) - safety margin on top of forecasted values * [`forecast_interval`](/reference/ast-definition#modelforecast_interval) - how frequently forecasts are generated (duration string, e.g. `15m`, `1h`) ## Model Training Models continuously adapt to a workload's usage patterns, becoming better tuned for a workload over time. ### Training Period * **Newly Enrolled AiScaleTarget:** Models begin learning a workload's usage patterns after collecting initial workload metrics. Initial forecasts are based on a small sample size of workload usage data, and will improve as the data sample size increases. AiScaleTargets require 24 hours of metrics by default before being eligible for autonomous scaling. This is controlled by the Helm value `thorasForecast.minLookbackToScale` and can be overridden per-AST via `model.min_lookback_to_scale`. The hard minimum is 3 hours. * **Minimum:** Allow at least 24 hours in `recommendation` mode before switching to `autonomous` mode. * **Optimal:** 2 weeks of data provides the best accuracy, especially for workloads with weekly patterns. ### Data Requirements For accurate forecasts, Thoras needs: * Consistent metric collection from workloads * Representative traffic patterns (including peak and off-peak periods) ## Container Startup Resource Spikes Software often experiences temporary spikes in CPU and memory usage during initialization. These spikes can significantly exceed the container's steady-state operating resource consumption. ### How Forecasting Handles This Newly started containers are automatically detected. Metrics from newly started containers are excluded from forecasts by default, giving the container time to reach steady-state operation. The forecaster then trains on adjusted metrics that represent your workload's actual operational resource usage, resulting in: * More accurate resource predictions and reduced thrashing * Right-sized resource allocations * Reduced infrastructure costs ### Viewing Startup Spikes In the dashboard, you can toggle "Show restarts" on the Actual vs Predicted Usage chart to see the difference between: * **Adjusted usage** (default) - Resource usage with startup spikes filtered out. This adjusted data is what the forecaster uses to make predictions. * **Actual usage** - Raw resource usage including all startup spikes. This helps visualize how forecasting excludes startup spikes. ### Opting Out Of Excluding Startup Metrics From Forecasts You may opt-out of excluding metrics for newly started containers from forecasting. Note that opting out may increase the volatility of both usage and forecasts, especially for low-usage workloads. To include startup metrics in forecast model training, set `ignoreNewPods` to `false` in your Helm values: ```yaml theme={null} forecaster: # true (default): exclude metrics for newly started containers from model training. # false: include metrics for newly started containers in model training. ignoreNewPods: false ``` # Pod Annotations Source: https://docs.thoras.ai/reference/pod-annotations Reference for the thoras.ai/ annotations Thoras writes to pods, and what they do and don't guarantee. ## Overview When the vertical scaling admission webhook mutates a pod's resource requests (or limits) in `autonomous` mode, it annotates the pod: ```yaml theme={null} metadata: annotations: thoras.ai/managed: "true" thoras.ai/scaler: "prod/api-server" thoras.ai/admitted-at: "2026-08-19T14:02:11Z" ``` | Annotation | Description | | - | - | | `thoras.ai/managed` | `"true"` if the webhook wrote at least one resource request or limit on this pod. | | `thoras.ai/scaler` | `/` of the `AIScaleTarget` or `DaemonSetAutoscaler` that drove the mutation. | | `thoras.ai/admitted-at` | RFC 3339 timestamp of when the webhook mutated the pod, set once at admission. | These are written once, at pod admission (`CREATE`), by the mutating webhook only — not by [in-place resize](/guides/vertical-pod-rightsizing#update-policy), which patches running pods directly without going through admission. This is scoped to the pod-mutating webhook, which covers both vertical `AIScaleTarget` rightsizing and initial resource seeding by a `DaemonSetAutoscaler`. Horizontal scaling (replica count), which mutates the `*/scale` subresource instead of the pod, is a separate webhook and does not set these annotations. ## What This Answers `thoras.ai/managed: "true"` means **"the webhook set this pod's resources at admission,"** and nothing stronger. Use it to answer: did Thoras's mutating webhook write to this pod's `resources`, as opposed to the controller's pod template, another mutating webhook, or a manual `kubectl edit`. It does **not** mean "Thoras currently manages this pod." Two situations produce a pod that is annotated `managed: "true"` but is no longer representative of Thoras's current recommendation: ### Pods Predate Autonomous Mode An `AIScaleTarget` switched into `autonomous` mode only annotates pods that are admitted (created) after the switch. Pods that were already running keep being resized in place by the disruption worker and are never re-admitted, so they stay unannotated until they're naturally recreated (a rollout, eviction, node drain, or manual restart). Once a pod turns over, it goes through the webhook and picks up the annotation on its next admission. ### The Annotation Isn't Cleared When Thoras Stops Managing a Pod Switching an `AIScaleTarget` out of `autonomous` mode, or unenrolling it (see [Stopping Autonomous Scaling](/guides/stopping-autoscaling)), reverts running pods to their controller's template values with an in-place resize wherever possible — the pods are not recreated, so they are never re-admitted and the webhook never runs again to clear the annotation. A reverted pod keeps `thoras.ai/managed: "true"` while sitting at template values. As with the case above, this self-heals the next time the pod is recreated for any reason: a fresh admission with nothing to mutate does not set the annotation. When the in-place revert can't apply (for example, the template declares a different set of resource keys than the running pod, which would change its QoS class) and the update policy permits recreation, Thoras falls back to restarting the workload — those pods are recreated and come back without the annotation. Both cases are false positives on a pod Thoras did mutate at some earlier admission, consistent with the annotation's actual meaning — never false positives claiming Thoras is currently managing a pod it isn't. ## Related Documentation * [Predictive Vertical Pod Rightsizing](/guides/vertical-pod-rightsizing) - Configuring `autonomous` mode and update policy * [Stopping Autonomous Scaling](/guides/stopping-autoscaling) - How pods revert when autonomous mode is turned off * [ClusterAIScaleTemplate](/reference/cluster-aiscale-template) - The `thoras.ai/skip-template` and `thoras.ai/managed-by-template` annotations and labels, which control template enrollment rather than pod mutation