A taint repels Pods that do not tolerate its effect. A toleration removes that obstacle; it does not force placement onto specialized hardware. Dedicated-node designs usually combine a taint with a label and required node affinity, then check capacity and admission policy separately.

Untolerated taints have different effects: NoSchedule blocks new ordinary scheduling, PreferNoSchedule softly avoids it, and NoExecute also triggers eviction of bound Pods. A matching NoExecute toleration may expire after tolerationSeconds. Matching uses key, operator, value and effect. Equal checks value, Exists ignores the selected key’s value, empty effect covers all effects, and a valid empty-key Exists broadens keys. All hard taints must be tolerated for this filter to pass. Capacity, required affinity, device requests, authorization and admission are separate. The article Python fixture checks matching only; no cluster behavior is validated here.
A toleration removes a matching taint obstacle. The effect determines the consequence; placement and authorization remain separate.

A sign for specialists

A NoSchedule taint blocks new ordinary scheduling when it is not tolerated; it leaves existing Pods alone. This can reserve GPU or other specialist nodes from general workloads. The stronger claim that every taint leaves running Pods untouched is false: NoExecute has eviction behavior, while PreferNoSchedule is only a scheduling preference.

Taints are not security isolation. Someone allowed to submit suitable tolerations may qualify for the pool, and directly assigning nodeName bypasses ordinary scheduler checks. Admission restrictions, authorization and the workload’s requested devices remain separate controls. A GPU toleration alone neither requests a GPU nor guarantees that one is available.

Effect decides the action

NoSchedule is a hard scheduling exclusion for untolerated Pods. PreferNoSchedule contributes a soft avoidance preference and cannot alone make a node ineligible. NoExecute can also trigger removal of already-bound Pods without a matching toleration. A matching NoExecute toleration may specify tolerationSeconds to limit how long the Pod remains after the taint appears.

The taint eviction controller, rather than the scheduler moving running processes, handles taint-based eviction. This is not equivalent to kubectl drain: disruption-budget handling and operational sequencing differ. Use the intended maintenance workflow and assess affected workloads before applying an eviction-producing taint.

Pair is a contract

For the Equal and Exists operators illustrated here, key, operator, value and effect determine matching. Equal checks the value; Exists ignores value for the selected key. An empty effect covers all effects. A permitted empty key requires Exists and broadens matching across keys. Validate the API form rather than letting malformed input quietly become a wildcard.

After matching, every remaining hard taint must be tolerated for this scheduling filter to pass. Passing it is only one requirement: resource fit, affinity, volumes and other scheduler plugins can still reject the node. Required affinity to the dedicated-node label supplies the placement restriction that a toleration deliberately lacks.

This model follows the Kubernetes 1.34 Equal/Exists subset, with omitted string values normalized to empty strings. It is not full API validation, defaulting or admission. Later feature-gated operators require their own version-aware checks.

When it breaks

The most expensive failure is the over-broad toleration that pollutes the specialist pool. A toleration with operator: Exists and no value, or one copied into a shared chart, matches every value of that key, so ordinary Deployments suddenly qualify for GPU or spot nodes. The reserved pool fills with cheap pods while the specialists it was built for stay Pending. This is the mirror image of the DaemonSet failure: there a missing toleration leaves special nodes uncovered, here an extra toleration leaves them unprotected. Audit broad tolerations; they are not RBAC authorization.

The second failure is applying NoExecute when you meant NoSchedule. NoSchedule steers only new pods, while NoExecute evicts running pods without a matching toleration, optionally after tolerationSeconds expires. A maintenance taint with the wrong effect can unexpectedly evict production Pods. Unlike standard IgnoredDuringExecution node affinity, the taint effect carries destructive power, so rehearse on one node and watch kubectl get pods -o wide before repeating across the pool.

The third failure is spelling drift between taint and toleration. dedicated=gpu:NoSchedule never meets dedicated=GPU:NoSchedule, and a toleration naming PreferNoSchedule does not cover a NoSchedule taint. Specialists sit Pending with had taint that the pod didn't tolerate while the pool looks healthy. Compare both sides with kubectl describe pod against kubectl get node <name> -o jsonpath='{.spec.taints}', and keep the canonical triple in one documented place.

Check it quickly

Save the following as taint_toleration_sim.py and run python3 taint_toleration_sim.py. It uses only the standard library, touches no cluster, and models the rules above: key plus operator plus value plus effect, with soft taints never blocking.

def tolerates(taint, tol):
    """One toleration matches one taint on key, value, operator, and effect."""
    operator = tol.get("operator", "Equal")
    if operator not in {"Equal", "Exists"}:
        raise ValueError("model supports only Equal and Exists")
    if not tol.get("key") and operator != "Exists":
        raise ValueError("empty key requires Exists")
    if operator == "Exists" and tol.get("value"):
        raise ValueError("Exists requires an empty value")
    if tol.get("key") and tol["key"] != taint["key"]:
        return False
    if tol.get("operator", "Equal") == "Equal":
        if tol.get("value", "") != taint.get("value", ""):
            return False
    effect = tol.get("effect", "")
    if effect and effect != taint.get("effect"):
        return False
    return True


def schedulable(taints, tolerations):
    """A pod schedules only if every hard taint is tolerated."""
    for taint in taints:
        if taint["effect"] == "PreferNoSchedule":
            continue
        if not any(tolerates(taint, tol) for tol in tolerations):
            return False
    return True


def check(actual, expected, message):
    if actual != expected:
        raise AssertionError(f"{message}: got {actual}, want {expected}")
    print(f"OK: {message} -> {actual}")


def main():
    gpu = {"key": "dedicated", "value": "gpu", "effect": "NoSchedule"}
    exact = {"key": "dedicated", "value": "gpu", "effect": "NoSchedule"}
    check(schedulable([gpu], [exact]), True, "exact match schedules")
    check(schedulable([gpu], []), False, "missing toleration is repelled")
    wide = {"key": "dedicated", "operator": "Exists", "effect": "NoSchedule"}
    other = {"key": "dedicated", "value": "spot", "effect": "NoSchedule"}
    check(schedulable([other], [wide]), True, "Exists toleration matches any value")
    check(schedulable([gpu], [{"key": "dedicated", "value": "spot", "effect": "NoSchedule"}]), False, "wrong value is repelled")
    evict = {"key": "maintenance", "value": "drain", "effect": "NoExecute"}
    any_effect = {"key": "maintenance", "value": "drain"}
    check(schedulable([evict], [any_effect]), True, "empty effect matches any effect")
    check(schedulable([evict], []), False, "untolerated NoExecute fails the scheduling filter")
    soft = {"key": "cost", "value": "cheap", "effect": "PreferNoSchedule"}
    check(schedulable([soft], []), True, "PreferNoSchedule never blocks")
    print("PASS: 7 scenarios; allow and repel verified")


if __name__ == "__main__":
    main()

The seven scenarios show that an exact toleration passes this model’s taint filter while a missing one repels, an Exists toleration matches any value but a wrong value is refused, an empty effect covers NoExecute, an untolerated NoExecute fails this filter; no actual eviction is simulated, and PreferNoSchedule never blocks. Expected output ends with PASS: 7 scenarios; allow and repel verified.

Both language fixtures were rerun with Python 3.12.14 on Linux. Additional checks rejected malformed tolerations and exercised empty-key and effect-mismatch controls. The script checks matching logic only, not capacity, affinity, or live taints. Before changing production, compare pod events against the node’s actual taint list.

Further reading

Sources