> ## 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.

# Manage node pools with the APC API

> Read, create, update, and delete the node pools that worker queues are placed on, using the APC API.

> [!NOTE]
>
> The examples on this page show common ways to use the APC API. They're not a complete API reference. For the full, interactive API documentation, including every query, mutation, and type, go to `https://houston.<your-base-domain>/v1`, and click the **Docs** tab. See [Develop and test APC API queries](https://astronomer.io/docs/astro-private-cloud/v-2-x/houston-api-develop-test.md) for more on using the built-in GraphQL explorer.

A node pool describes a node group that already exists in a data plane cluster, so you can place [worker queues](https://astronomer.io/docs/astro-private-cloud/v-2-x/configure-worker-queues.md) on it. For what node pools do and how to manage them from the UI, see [Configure node pools](https://astronomer.io/docs/astro-private-cloud/v-2-x/node-pools.md).

> [!NOTE]
>
> **Astro Private Cloud 2.2**
>
> This feature was introduced in Astro Private Cloud 2.2. To access this feature, upgrade your Astro Private Cloud installation to 2.2 or later.

These operations need the `system.nodePools.*` or `cluster.nodePools.*` permissions. See [Permissions](https://astronomer.io/docs/astro-private-cloud/v-2-x/node-pools.md#permissions).

## List node pools on a cluster

```graphql expandable wrap theme={null}
query nodePools($clusterId: Uuid!) {
  nodePools(clusterId: $clusterId) {
    id
    name
    description
    nodeSelector
    tolerations {
      key
      operator
      value
      effect
      tolerationSeconds
    }
    maxCpu
    maxMemory
    maxEphemeralStorage
    maxNodeCount
    workerQueueCount
    capacity {
      cpu { budget committed }
      memory { budget committed }
      ephemeralStorage { budget committed }
      overCommitted
    }
  }
}
```

`workerQueueCount` is how many worker queues reference the pool. `capacity.overCommitted` is `true` when those queues, counted at their maximum worker counts, request more than the pool's total. Over-commitment is a warning, not a block.

`capacity` accepts an `excludeDeploymentId` argument, so a client editing one Deployment can show the pool's committed total without double-counting that Deployment's queues.

To see which queues are on a pool, request `workerQueueClaims`. The field returns `null` for a caller without node pool read permission:

```graphql wrap theme={null}
query nodePools($clusterId: Uuid!) {
  nodePools(clusterId: $clusterId) {
    name
    workerQueueClaims {
      deploymentId
      releaseName
      workerQueueName
      cpu
      memory
      ephemeralStorage
      maxWorkers
    }
  }
}
```

## Create a node pool

```graphql wrap theme={null}
mutation createNodePool($clusterId: Uuid!, $input: InputNodePool!) {
  createNodePool(clusterId: $clusterId, input: $input) {
    id
    name
    nodeSelector
    maxCpu
    maxMemory
  }
}
```

With these variables:

```json expandable wrap theme={null}
{
  "clusterId": "<cluster-id>",
  "input": {
    "name": "compute-optimized",
    "description": "Nodes reserved for ETL workloads",
    "nodeSelector": {
      "node-role": "etl"
    },
    "tolerations": [
      {
        "key": "workload",
        "operator": "Equal",
        "value": "airflow",
        "effect": "NoSchedule"
      }
    ],
    "maxCpu": 15000,
    "maxMemory": 30720,
    "maxEphemeralStorage": 51200,
    "maxNodeCount": 12
  }
}
```

## InputNodePool fields

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | `String!` | Yes | Unique within the cluster. The same name on another cluster is allowed |
| `description` | `String` | No | Free-text note. Not used for scheduling |
| `nodeSelector` | `JSON!` | Yes | A flat object of string label keys to string values. `{}` means any node |
| `tolerations` | `[InputToleration!]` | No | One entry per taint on these nodes |
| `maxCpu` | `Int` | No | CPU one node gives a worker, in millicores |
| `maxMemory` | `Int` | No | Memory one node gives a worker, in MiB |
| `maxEphemeralStorage` | `Int` | No | Ephemeral storage one node gives a worker, in MiB |
| `maxNodeCount` | `Int` | No | How many of these nodes worker queues can use |

Each `max*` value must be a whole number greater than zero. Leaving one unset means the pool imposes no ceiling on that dimension, and only the platform [resource limits](https://astronomer.io/docs/astro-private-cloud/v-2-x/configure-component-size-limits.md) apply.

### `InputToleration` fields

| Field | Type | Description |
| - | - | - |
| `key` | `String` | The taint key. Leave it unset only with the `Exists` operator |
| `operator` | `TolerationOperator` | `Exists` or `Equal` |
| `value` | `String` | Must be empty when the operator is `Exists` |
| `effect` | `TolerationEffect` | `NoSchedule`, `PreferNoSchedule`, or `NoExecute`. Leave it unset to match every effect |
| `tolerationSeconds` | `Int` | Applies only to the `NoExecute` effect |

## Update a node pool

```graphql wrap theme={null}
mutation updateNodePool($nodePoolId: Uuid!, $input: InputNodePool!) {
  updateNodePool(nodePoolId: $nodePoolId, input: $input) {
    id
    name
    maxCpu
    pendingConvergenceDeploymentIds
  }
}
```

`name` and `nodeSelector` are non-null on `InputNodePool`, so every update must resend both. A call that sends only `{ "maxCpu": null }` fails schema validation.

The optional fields behave as a partial patch, unlike `InputWorkerQueue` on `upsertDeployment`:

* An optional field you omit keeps its stored value.
* An optional field you set to `null` is cleared.

Clear a ceiling with an explicit `null`. This is the only way to fix a mis-set ceiling, because a pool that's in use can't be deleted and recreated.

Renaming a pool is safe. Worker queues reference a pool by its identifier, so a rename can't orphan them. A name that collides with another pool on the same cluster is refused.

### Converge Deployments after a placement change

Changing `nodeSelector` or `tolerations` doesn't reach Deployments that already use the pool until you rebuild each one. `pendingConvergenceDeploymentIds` reports which Deployments aren't running the pool's current placement, without reading resources back from the cluster.

Rebuild exactly those Deployments:

```graphql wrap theme={null}
mutation upgradeDeploymentsForNodePoolConvergence($nodePoolId: Uuid!) {
  upgradeDeploymentsForNodePoolConvergence(nodePoolId: $nodePoolId) {
    success
    upgradedDeploymentIds
    skipped {
      deploymentId
      reason
    }
  }
}
```

This mutation never touches the rest of the cluster.

## Delete a node pool

```graphql wrap theme={null}
mutation deleteNodePool($nodePoolId: Uuid!) {
  deleteNodePool(nodePoolId: $nodePoolId) {
    id
    name
  }
}
```

A pool that any worker queue references can't be deleted. The error names the Deployments and queues that still use it. Move or remove those queues first.

Node pools are hard-deleted, with no soft delete and no recovery.

## Errors

| Error | Cause |
| - | - |
| `ResourceNotFoundError` | The `nodePoolId` or `clusterId` doesn't exist |
| `NodePoolInUseError` | You tried to delete a pool that worker queues still reference. The message lists the Deployment and queue for each |
| `NodePoolCeilingBelowWorkerQueuesError` | You lowered a ceiling below what an existing queue requests. The message names every offending Deployment, queue, field, requested value, and new ceiling |
| `Node pool "<name>" already exists on this cluster` | A name collision within the cluster |
| `Node pool nodeSelector must be an object of label keys to string values` | `nodeSelector` isn't a flat string-to-string object |
| `Toleration N: operator must be Exists when no key is given, because an empty key matches every taint` | A toleration has no key and an operator other than `Exists` |
| `Toleration N: value must be empty when the operator is Exists` | A value was supplied with the `Exists` operator |
| `Toleration N: tolerationSeconds only applies to the NoExecute effect` | `tolerationSeconds` was set on another effect |
| `Node pool <field> must be greater than 0` | A `max*` field is zero or negative |

Exceeding a pool's total budget doesn't produce an error. It sets `capacity.overCommitted` to `true`.

## See also

* [Configure node pools](https://astronomer.io/docs/astro-private-cloud/v-2-x/node-pools.md)
* [Manage worker queues with the APC API](https://astronomer.io/docs/astro-private-cloud/v-2-x/houston-manage-worker-queues.md)
* [Role and permission reference](https://astronomer.io/docs/astro-private-cloud/v-2-x/role-permission-reference.md)
