Overview

Traces what happens when an owner object is deleted — how ownerReferences drive cascade deletion, how the garbage collector controller resolves dependency graphs, and how finalizers block or gate cleanup.

Garbage Collection Architecture

Kubernetes GC is graph-based: every object can declare owners via ownerReferences.
When an owner is deleted, dependents are collected — either immediately (foreground)
or in the background.

  Owner                 Dependent
  ┌─────────┐           ┌───────────────────┐
  │Deployment│──owns───►│ReplicaSet         │──owns───►│Pod│
  └─────────┘           │ownerRef:          │          └───┘
                        │  name: payments-api│
                        │  uid: abc-123      │
                        └───────────────────┘

GC Controller (in kube-controller-manager):
  - Maintains an in-memory directed acyclic graph of all objects + ownerRefs
  - Watches ALL object types via informers
  - When owner deleted → enqueues dependents for deletion

Garbage Collection Sequence — Background Deletion (Default)

kubectl           API Server         etcd        GC Controller
   │                  │               │               │
   │─DELETE deploy────►│               │               │
   │  (no propagation  │               │               │
   │   policy set)     │               │               │
   │                   │──DELETE ─────►│               │
   │◄── 200 OK ────────│               │               │
   │                   │               │               │
   │                   │──WATCH event ──────────────►  │
   │                   │  (Deployment Deleted)          │
   │                   │                               │
   │                   │                    ┌─── GC controller ────────────┐
   │                   │                    │ Look up dependents of        │
   │                   │                    │ Deployment uid=abc-123:      │
   │                   │                    │   RS: payments-api-v2 (bound)│
   │                   │                    │   RS: payments-api-v1 (old)  │
   │                   │                    └──────────────────────────────┘
   │                   │                               │
   │                   │◄── DELETE RS payments-api-v2 ─│
   │                   │◄── DELETE RS payments-api-v1 ─│
   │                   │──DELETE RSs ──────►│           │
   │                   │                   │           │
   │                   │──WATCH event ──────────────►  │
   │                   │  (ReplicaSet Deleted)          │
   │                   │                               │
   │                   │                    ┌─── GC: RS dependents ───────┐
   │                   │                    │ Pods owned by RS:            │
   │                   │                    │   pod-xxx, pod-yyy, pod-zzz  │
   │                   │                    └──────────────────────────────┘
   │                   │◄── DELETE pod-xxx, yyy, zzz ──│
   │                   │──DELETE pods ─────►│           │
   │                   │                   │           │
   │                   Deployment → RSs → Pods all gone
   │                   (async, order not guaranteed)

Foreground Deletion Sequence

kubectl           API Server         etcd        GC Controller
   │                  │               │               │
   │─DELETE deploy────►│               │               │
   │  propagationPolicy│               │               │
   │  : Foreground     │               │               │
   │                   │               │               │
   │           ┌─── API Server sets ──────────────┐   │
   │           │  metadata.deletionTimestamp: now  │   │
   │           │  metadata.finalizers:             │   │
   │           │    [foregroundDeletion]            │   │
   │           │  (object NOT deleted from etcd     │   │
   │           │   until finalizer removed)         │   │
   │           └───────────────────────────────────┘   │
   │◄── 200 OK (object is "terminating") ─────────────│
   │                   │               │               │
   │                   │──WATCH ────────────────────►  │
   │                   │  (Deployment Modified —        │
   │                   │   deletionTimestamp set)       │
   │                   │                               │
   │                   │                    ┌─── GC: foreground ──────────┐
   │                   │                    │ Find all "blocking" owners   │
   │                   │                    │ (ownerRef.blockOwnerDeletion=true)
   │                   │                    │ → RS payments-api-v2         │
   │                   │                    │ Delete RSs first             │
   │                   │                    │ RS deletes its Pods          │
   │                   │                    │ Once all dependents gone:    │
   │                   │                    │ PATCH deploy.finalizers = [] │
   │                   │                    └──────────────────────────────┘
   │                   │◄── PATCH finalizers=[] ───────│
   │                   │──DELETE deploy ───►│           │
   │                   │                   │           │
   │                   Deployment deleted only AFTER all dependents gone
   │                   (kubectl delete blocks until owner is gone)

Orphan Deletion

kubectl delete deployment payments-api \
  --cascade=orphan   (or propagationPolicy: Orphan)

Effect:
  - Deployment deleted immediately
  - ReplicaSets and Pods have their ownerReference REMOVED
  - They become "orphaned" — no owner — and continue running
  - GC controller does NOT delete orphaned objects

Use cases:
  - Adopt existing pods with a new Deployment
  - Delete a StatefulSet without destroying its PVCs
  - Debug: keep pods running after deleting the controller

ownerReferences in Practice

# ReplicaSet automatically created with ownerReference to Deployment
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: payments-api-abc123
  ownerReferences:
  - apiVersion: apps/v1
    kind: Deployment
    name: payments-api
    uid: abc-123-def-456        # UID, not just name — immutable across recreates
    controller: true            # this owner controls the object
    blockOwnerDeletion: true    # this RS blocks foreground deletion of Deployment
ownerReference rules:
  - uid must match the owner's current UID
    (prevents stale references after recreate with same name)
  - Cross-namespace ownerReferences are not allowed
    (Kubernetes GC rejects them — namespace-scoped objects cannot own cluster-scoped)
  - Multiple owners allowed (but only one controller:true per object)

Finalizers — Blocking Deletion

Finalizers are strings in metadata.finalizers[].
An object with finalizers set will NOT be deleted from etcd
until all finalizers are removed, regardless of deletionTimestamp.

Flow:
  kubectl delete secret payments-db-creds
    → API server: sets deletionTimestamp, leaves finalizers intact
    → External controller watching for deletionTimestamp:
        performs cleanup (rotate secret in Vault, notify audit system)
        PATCH secret: metadata.finalizers = []
    → API server sees empty finalizers + deletionTimestamp set
    → Deletes object from etcd
# External secrets controller pattern: finalizer on Secret
apiVersion: v1
kind: Secret
metadata:
  name: payments-db-creds
  namespace: production
  finalizers:
  - secrets.external-secrets.io/cleanup    # registered by ESO controller
  # When this secret is deleted:
  # 1. ESO controller sees deletionTimestamp
  # 2. Removes the external secret from Vault
  # 3. Removes this finalizer
  # 4. K8s deletes the Secret object

GC Controller Internals

Graph builder (goroutine):
  Watches ALL object types
  On Add:   insert node, link to owner nodes
  On Update: update edges (ownerRefs changed)
  On Delete: mark node as "virtual deleted" (may have dependents)

GC processor (goroutine):
  Processes "dirty queue" of nodes to potentially delete
  For each dirty node:
    if node has no owner AND no parent in graph → skip (root object)
    if all owners are deleted → delete this node
    if owner exists but ownerRef.uid mismatch → ownerRef is stale → treat as orphan

Resync: full graph rebuild every 30 seconds to catch missed watch events

Cascading Delete Comparison

ModeFlag / PolicyOwner deleted whenDependents deleted when
Background (default)--cascade=backgroundImmediatelyAsynchronously by GC
Foreground--cascade=foregroundAfter all blocking dependents goneSynchronously before owner
Orphan--cascade=orphanImmediatelyNever (ownerRef removed)

Debugging GC Issues

# Check if object is stuck terminating (has finalizers)
kubectl get pod payments-api-xxx -n production -o jsonpath='{.metadata.finalizers}'

# Check deletionTimestamp
kubectl get deploy payments-api -n production \
  -o jsonpath='{.metadata.deletionTimestamp}'

# Force-remove a finalizer (last resort — bypasses controller cleanup)
kubectl patch pod payments-api-xxx -n production \
  -p '{"metadata":{"finalizers":[]}}' --type=merge

# Check owner references on a ReplicaSet
kubectl get rs -n production -o json | \
  jq '.items[].metadata | {name:.name, owners:.ownerReferences}'

# List orphaned ReplicaSets (no owner Deployment)
kubectl get rs -n production -o json | \
  jq '.items[] | select(.metadata.ownerReferences == null) | .metadata.name'

# GC controller logs
kubectl logs -n kube-system -l component=kube-controller-manager \
  --tail=100 | grep -i "garbage\|gc\|orphan"

# Check if GC is keeping up
kubectl get --raw /metrics | grep garbagecollector

Common GC Problems

Problem: Object stuck terminating
  Cause: finalizer registered but controller that owns it is gone
  Fix: identify which controller handles the finalizer, restore it
       or manually remove finalizer as last resort

Problem: Orphaned Pods after Deployment delete
  Cause: used --cascade=orphan intentionally or a bug in controller
  Fix: kubectl delete pods -l app=payments-api -n production

Problem: PVC not deleted after StatefulSet delete
  Cause: StatefulSets intentionally do NOT own their PVCs
         (prevents accidental data loss on StatefulSet delete)
  Fix: PVCs must be deleted manually after StatefulSet removal

Problem: ReplicaSet not garbage collected
  Cause: ownerReference.uid mismatch (Deployment was recreated with same name)
  Fix: old RS still has uid of deleted Deployment; GC cannot find owner
       → manually delete stale RS