> ## Documentation Index
> Fetch the complete documentation index at: https://astronomer.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Control plane reliability reference

This page is the reference companion to [Control plane reliability](/docs/astro-private-cloud/v-2-x/control-plane-disaster-recovery). It documents the Helm values, cookie and URL strategies, identity provider (IdP) redirect URLs, database connection sizing, control plane statuses, and error codes for control plane reliability. For the setup procedure, see [Configure control plane reliability](/docs/astro-private-cloud/v-2-x/configure-control-plane-disaster-recovery). For day-2 operations, see [Manage a control plane reliability group](/docs/astro-private-cloud/v-2-x/manage-control-plane-disaster-recovery).

## Helm values

Set these values in your Astronomer Helm values. `controlPlaneHA.enabled` and `dataPlaneFailover.enabled` are independent switches, so you can run control plane reliability without data plane failover, or the other way around.

| Value                                           | Default                     | Effect                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `global.controlPlaneHA.enabled`                 | `false`                     | Set to `true` on every control plane in the group. Turns on control plane reliability: renders the global-domain ingresses, mounts the per-control-plane (per-CP) identity (`cp-identity` secret), enables the control plane registry, the `/controlplane/status` health endpoint, and chart-version-eligibility gating, scopes session cookies and URLs to `globalBaseDomain`, and surfaces the **Regions** and **Control Planes** admin tabs. Requires `globalBaseDomain`, a shared database, and a shared JSON Web Token (JWT) keypair. |
| `global.controlPlaneHA.bootstrapJwks`           | `false`                     | Set to `true` on the first control plane only, and `false` on all others. On the first control plane, generates the shared JWT signing key and certificate. On subsequent control planes, leave it `false` so that they consume the copied keypair — a fresh key would break cross-control-plane token validation.                                                                                                                                                                                                                         |
| `global.controlPlaneHA.globalBaseDomain`        | Unset                       | The shared customer domain that every control plane serves (`app.<globalBaseDomain>`, `houston.<globalBaseDomain>`). Drives the global ingress hosts and the session-cookie scope (`.<globalBaseDomain>`).                                                                                                                                                                                                                                                                                                                                 |
| `global.baseDomain`                             | Required, per control plane | This control plane's own admin hostname (`cp01.<parent-domain>`), distinct from `globalBaseDomain`. Used to reach a specific control plane directly, bypassing the global load balancer.                                                                                                                                                                                                                                                                                                                                                   |
| `global.dataPlaneFailover.enabled`              | `false`                     | Independent of `controlPlaneHA`. On a control plane, enables the components that let Astro Deployments be failed over between clusters. On a data plane, enables the data plane execution components. See [Data plane failover](/docs/astro-private-cloud/v-2-x/data-plane-failover).                                                                                                                                                                                                                                                           |
| `astronomer.houston.upgradeDeployments.enabled` | `true`                      | The `houston-upgrade-deployments` Helm hook (`post-upgrade`, `post-install`). When enabled, every control plane `helm upgrade` re-stamps all non-cordoned Astro Deployment ingresses. Set to `false` to skip the automatic re-stamp and run it manually. See [Add existing Astro Deployments to the global domain](/docs/astro-private-cloud/v-2-x/configure-control-plane-disaster-recovery#add-existing-astro-deployments-to-the-global-domain).                                                                                              |

<Note>
  Set `astronomer.houston.upgradeDeployments.enabled` under the `astronomer` subchart, as shown. A top-level `houston.upgradeDeployments.enabled` doesn't take effect and silently leaves the hook enabled.
</Note>

## Cookie and URL strategy

When you bring existing Astro Deployments under control plane reliability, you choose how their URLs and the session cookie are scoped. APC supports three strategies, all selected through Helm and DNS configuration on an APC 2.1.0 or later build. Pick one per installation.

### Alias existing URLs

Option A is the recommended default for existing installations. Keep every Astro Deployment's existing per-CP URL and add a parallel global-domain URL as an alias. Both resolve to the same Airflow webserver.

* *Enable*: the default path. Set `controlPlaneHA.enabled: true` and `globalBaseDomain`. The [ingress re-stamp](/docs/astro-private-cloud/v-2-x/configure-control-plane-disaster-recovery#add-existing-astro-deployments-to-the-global-domain) adds the global-alias hosts alongside the per-CP ones.
* *Cookie*: the session cookie is scoped to `.<globalBaseDomain>`, so it's sent to Deployment Airflow subdomains under the global domain and single sign-on (SSO) works there. Per-CP hosts keep their own per-CP cookie.
* *Customer impact*: none. Old per-CP URLs keep working, and global URLs are added. This is the safe default for existing customers.

### Widen the cookie scope

With Option B, scope one session cookie to a common ancestor domain that covers both the global and per-CP host families, so a single cookie is sent to every host under that ancestor.

* *Enable*: set `helm.cookieDomain` to the shared parent of your global and per-CP domains. The platform then runs in single-cookie mode, in which per-CP hosts mint the same wide cookie.

<Warning>
  The cookie is sent to every host under the ancestor, including anything else hosted there. There is no platform guard against too broad an ancestor. You're responsible for choosing one that covers only trusted Astronomer hosts, and for TLS covering those hosts. Choose Option B only if that broader exposure is acceptable.
</Warning>

### Re-align URLs

Option C is for fresh installations only. Regenerate all Astro Deployment URLs under a single new shared parent domain, replacing the per-CP URLs rather than aliasing them.

* *Enable*: install with the cluster base domain set to the shared parent so that every URL templates under it.

<Warning>
  Because per-CP URLs are replaced rather than kept, any tooling that uses the old hardcoded URLs breaks. For that reason, Option C is supported only for fresh installations, never as a migration for existing customers.
</Warning>

## Identity provider authentication and the OAuth redirect URL

To configure an external identity provider on a standard single-control-plane installation, see [Integrate an authentication system](/docs/astro-private-cloud/v-2-x/integrate-auth-system). This section covers only what changes when you enable control plane reliability with `controlPlaneHA.enabled: true`.

If you integrate an external identity provider (IdP), the redirect and callback URL you register with the IdP must point at the global domain, not a per-CP domain. Whenever `controlPlaneHA.enabled: true`, the APC API templates all customer-facing URLs — including the OAuth `redirect_uri` it hands to your IdP — from `globalBaseDomain` instead of the per-CP `baseDomain`.

Register the values that use the global domain:

* `https://houston.<global-domain-name>/v1/oauth/redirect/` for the implicit flow, which is the default.
* `https://houston.<global-domain-name>/v1/oauth/callback/` for the code flow (`auth.openidConnect.flow: "code"`).

Register whichever matches your configured flow, or both if you're unsure, and keep the trailing `/` as the APC API emits it, because most IdPs match the redirect URI exactly.

Customer sign-in and the IdP round trip run on the global hostname so that APC sets the session cookie, scoped to `.<global-domain-name>`, on a host that's allowed to set it. A per-CP host is a sibling of the global domain, not a descendant, so a global-scoped cookie set there is dropped and the user is bounced back to the sign-in page. To enforce this, the APC API doesn't serve customer auth paths on the per-CP admin hostnames: any OAuth start, IdP callback, or Deployment sign-in request that lands on a per-CP host is redirected to `app.<global-domain-name>/login`. The per-CP hostnames are for direct admin access only.

There is no separate Helm value for the redirect URL. It follows the same `globalBaseDomain` resolution as every other customer-facing URL. The session cookie's domain defaults to `.<global-domain-name>` and is overridable through `helm.cookieDomain`. See [Widen the cookie scope](#widen-the-cookie-scope).

<Warning>
  When you migrate an existing installation to control plane reliability, if your IdP application was registered with a per-CP redirect URI, update it to the global-domain URI as part of enabling control plane reliability. Otherwise the IdP rejects the callback after sign-in once the APC API starts sending the global `redirect_uri`.
</Warning>

<Note>
  This section applies to bring-your-own OIDC and IdP setups. If you use the default shared Auth0 tenant that Astronomer provides, rather than your own external IdP, the redirect is fixed to `https://redirect.astronomer.io` regardless of domain, and this section doesn't apply.
</Note>

## Database connection sizing

Every control plane shares one database, and each long-lived APC API Pod holds its own connection pool against it. As you add control planes or scale replica counts, total connections grow. Size the database's `max_connections`, or cap the pools, accordingly, or you risk connection exhaustion.

### Measured consumption

Measured on a two-control-plane installation with `astronomer.houston.prismaConnectionLimit` unset, so each Pod uses the default pool of about `num_cpus × 2 + 1`:

* About four connections per long-lived Pod.
* Long-lived, database-connected Pods per control plane at default replica counts, with control plane reliability and data plane failover enabled: two APC API, two APC API worker, three DP-Link, and three Navigator Pods, for 10 Pods and about 40 connections per control plane.
* Observed total for two control planes: 84 connections (about 42 per control plane), almost all idle, against `max_connections = 400` — about 21% utilized.
* Short-lived hook and cron jobs (database migration, `upgrade-deployments`, control plane refresh, and cleanup) open a few more connections briefly while they run. Budget a small margin for these.

### Capacity-planning formula

```text theme={null}
required_connections ≈ N_CPs × pods_per_CP × pool_per_pod
                      + margin for hook and cron jobs
                      + connections used by any other database on the instance
```

At default replica counts and default pool, `pods_per_CP × pool_per_pod` is about 40, so as a rule of thumb:

* Budget about 40 connections per control plane, and set `max_connections ≥ N_CPs × 40 × 1.25` for about 25% headroom.
* At `max_connections = 400`, that leaves headroom for roughly five to six control planes at default sizing before you must intervene.

Recompute if you change replica counts or increase Pod CPU, because the default pool scales with CPU (`num_cpus × 2 + 1` per Pod), so larger Pods open larger pools.

### Levers for more headroom

* *Cap the per-Pod pool*: set `astronomer.houston.prismaConnectionLimit` to a fixed, smaller value so that each Pod's pool is bounded regardless of Pod CPU. This makes the total exactly predictable: `N_CPs × pods_per_CP × prismaConnectionLimit`.
* *Front the database with PgBouncer* in transaction-pooling mode, so that many APC API Pods multiplex onto far fewer server-side connections. Astronomer recommends this once the control plane count or replica counts push you toward `max_connections`.
* *Raise `max_connections`* on the managed database if the instance class allows it. Each connection costs memory, so scale the instance accordingly.

Before you add a control plane, check current utilization and keep it comfortably below the limit, aiming for 75% or less:

```sql theme={null}
SELECT count(*) AS current,
       (SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_connections
FROM pg_stat_activity WHERE datname = current_database();
```

## Control plane status reference

| Effective status | Badge color | `/controlplane/status`                                      | Mutations                    |
| ---------------- | ----------- | ----------------------------------------------------------- | ---------------------------- |
| `ACTIVE`         | Green       | `200`, if registered, at maximum version, and region active | Accepted                     |
| `STANDBY`        | Blue        | `503` (`REGION_INACTIVE`)                                   | Rejected `REGION_INACTIVE`   |
| `CORDONED`       | Yellow      | `503` (`CORDONED`)                                          | Rejected `CP_CORDONED`       |
| `DECOMMISSIONED` | Gray        | `503` (`DECOMMISSIONED`)                                    | Rejected `CP_DECOMMISSIONED` |

## Health endpoint unhealthy reasons

The `/controlplane/status` endpoint evaluates the following reasons in order, and the first match wins:

1. Not registered (`NOT_REGISTERED`).
2. Cordoned (`CORDONED`).
3. Decommissioned (`DECOMMISSIONED`).
4. Region inactive (`REGION_INACTIVE`).
5. Chart version behind the region maximum (`VERSION_OUTDATED`).

A transient database error also returns unhealthy for that single poll, and this result isn't cached. Otherwise the endpoint returns `200`. Results are cached for about 30 seconds. The endpoint doesn't consider the last heartbeat — heartbeat staleness affects only the UI health dot, never routing.

## Error codes

These are the GraphQL error codes the APC API returns when it rejects a mutation. They're a separate set from the health-endpoint reasons in the previous section, which is why some names differ — for example, the health endpoint reports `VERSION_OUTDATED` while the matching mutation error is `CP_VERSION_OUTDATED`.

| Code                           | When it occurs                                                                                                                            |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `INVALID_CP_STATUS_TRANSITION` | You attempted an illegal status change, for example `ACTIVE` to `DECOMMISSIONED` directly.                                                |
| `CP_CORDONED`                  | You sent a mutation to a cordoned control plane.                                                                                          |
| `CP_DECOMMISSIONED`            | You sent a mutation to a decommissioned control plane.                                                                                    |
| `CP_VERSION_OUTDATED`          | You sent a mutation to a control plane behind its region's maximum chart version.                                                         |
| `REGION_INACTIVE`              | You sent a mutation to a control plane whose region isn't the active one.                                                                 |
| `TARGET_REGION_NOT_UPGRADED`   | `activateRegion` refused because no control plane in the target region is at the group-wide maximum version. Override with `force: true`. |
| `NOT_REGISTERED`               | The control plane has no registry row. Register it, or it was deregistered.                                                               |

## Related documentation

* [Control plane reliability](/docs/astro-private-cloud/v-2-x/control-plane-disaster-recovery)
* [Configure control plane reliability](/docs/astro-private-cloud/v-2-x/configure-control-plane-disaster-recovery)
* [Manage a control plane reliability group](/docs/astro-private-cloud/v-2-x/manage-control-plane-disaster-recovery)
* [Helm configuration reference](/docs/astro-private-cloud/v-2-x/helm-config-reference)
