Concepts
Dependencies
Keep a database awake while the apps that use it are, and wake it with them.
Databases, message brokers, caches and workers never see outside traffic. So their activity clock can run out while the workloads using them are busy. Napwake learns who needs what from each workload’s environment, or you declare it with dependsOn:
apiVersion: napwake.dev/v1alpha1kind: ManagedWorkloadmetadata: name: api namespace: preview-42spec: target: {kind: Deployment, name: api} dependsOn: - {kind: StatefulSet, name: postgres} # same namespace - {namespace: messaging, kind: StatefulSet, name: nats} # another namespace idlePolicy: idleAfter: 1h prediction: {} # the default, balanceddependsOn |
|
|---|---|
| Goes on | The workload that needs the other |
| Names | The dependency’s Deployment or StatefulSet, not its ManagedWorkload |
namespace |
Defaults to the ManagedWorkload’s own |
| As a label | napwake.dev/depends-on: "statefulset/postgres, messaging/statefulset/nats" on the workload. See Opting in |
The rules
Section titled “The rules”| Rule | |
|---|---|
| A dependency doesn’t pause while anything that depends on it is awake | Its own clock keeps running meanwhile |
| The last dependent pauses | The dependency pauses on its next check, if its own clock has run out |
| Waking a workload wakes its dependencies | Before it resumes, Napwake sets napwake.dev/last-activity on each dependency’s ManagedWorkload, the usual activity annotation wake |
| Seeing it | kubectl describe shows it |
| It chains | If Postgres depends on something, that wakes too |
| Everything wakes at once | By default, so a chain doesn’t add up to cold starts in a row |
| Waking a dependency never holds up its dependent | The dependent wakes, or is handed back or deleted, regardless |
| A dependency can’t be woken | As when a policy engine won’t let Napwake annotate its ManagedWorkload. The dependent gets a DependencyWakeFailed Warning event |
A dependency held awake says why:
HeldByDependents=True kept awake for preview-42/api, preview-43/apiWaiting for a dependency to be ready
Section titled “Waiting for a dependency to be ready”waitForReady keeps the dependent from starting until the dependency serves. Set it for an app that fails if it starts first, such as an API that gives up after a few retries while its database replays its write-ahead log:
dependsOn: - {kind: StatefulSet, name: postgres, waitForReady: true} - {kind: Deployment, name: cache} # wakes at the same timeThe dependent stays in Resuming, not scaled up:
WaitingForDependencies=True waiting for preview-42/statefulset/postgres (0 ready)| The dependency has | It serves |
|---|---|
| Every replica Ready | At once |
| Some replicas Ready, as with one pod crash-looping | With those, so the dependent doesn’t wait for good |
| Some Ready, and Napwake manages it | Once its own resume has finished, even short of every replica (ResumeIncomplete=True) |
| Some Ready, and Napwake doesn’t manage it | Once the dependent has waited 2 minutes, the time a resume gives its other replicas |
| No replica Ready | Never. It’s always waited for, up to the limit below |
| The wait | |
|---|---|
| Limit | The dependency Deployment’s progressDeadlineSeconds, 600 by default, when Kubernetes reports the rollout failed |
| Limit for a StatefulSet | 10 minutes, as a StatefulSet has none |
| Counted from | When the dependent began resuming |
| Why it’s long | A slow start is why you set it |
| Why a limit | Otherwise a dependency that’s up but never Ready would leave the dependent at zero for good |
| Never Ready, such as | Crash-looping, or with an image that can’t be pulled |
| Past the limit | The dependent starts anyway, with WaitingForDependencies=False, reason DependencyWontStart, and a Warning event naming the dependency |
| No wait, for an unmanaged dependency at zero | Waiting can’t start it. It’s reported the same way |
| No wait, for one that waits for the dependent in turn | Through waitForReady, directly or through others, as each would wait for the other |
| Then | Reason DependencyCycle, and both start together |
| While waiting | A managed dependency still paused is woken again every few seconds |
Past the limit, the condition reads:
WaitingForDependencies=False not waiting for preview-42/statefulset/postgres (it isn't Ready after 10 minutes)Edge cases
Section titled “Edge cases”| Situation | What happens |
|---|---|
| The dependency has no ManagedWorkload | It’s never paused, so there’s nothing to hold or wake |
| The dependency doesn’t exist | DependencyNotFound=True on the dependent. Nothing is blocked, a waitForReady resume included |
It’s outside watchNamespaces, or Napwake can’t read it |
DependencyNotFound=True, reason DependencyNotVisible, saying which. It isn’t held or woken, and nothing is blocked |
A cycle in dependsOn, A on B and B on A |
Each would hold the other awake forever. Neither pauses, and both report DependencyCycle=True until it’s removed |
| A workload listed as its own dependency | Ignored. It’s neither a cycle nor something to wait for |
A namespace’s napwake.dev/depends-on names several workloads, such as statefulset/postgres, statefulset/redis |
Its other workloads take the list. The ones it names don’t, so postgres and redis don’t depend on each other |
A named workload depends on another by its own annotation, as auth’s naming statefulset/postgres under a namespace naming deployment/auth |
That one doesn’t take the list either, so postgres doesn’t depend on auth |
| Learned dependencies in a loop, such as two services that call each other | Not a cycle. A learned dependent doesn’t hold a workload it’s itself a dependency of |
| Then | Each pauses when idle, and waking either wakes the other |
kubectl napwake pause on a dependency in use |
Not paused, as for an idle pause. PauseRequest=False, reason HeldByDependents, and a HeldByDependents event naming the dependents. Pause them first |
dryRun: true |
The hold applies and is reported, and nothing changes |
A dry-run dependent that would have paused (Idle) |
It stops holding, so a chain’s would-be pauses all count |
| A dependency that pauses for real, held by a dry-run dependent | It stays held, as the dependent’s pods are still up |
A dependent managing nothing, as one being deleted, a duplicate (DuplicateTarget=True), or one whose target is gone (TargetAvailable=False, reason TargetNotFound) |
It holds nothing up. There’s no running workload to keep them up for |
| Many dependents awake at once | HeldByDependents names the first 10, by namespace and name, and counts the rest |
Across namespaces
Section titled “Across namespaces”A dependency can be in another namespace if that namespace allows it. So preview environments can share a database or broker. List the namespaces allowed in its napwake.dev/dependents-from, or "*" for any:
kubectl annotate namespace shared napwake.dev/dependents-from="preview-42, preview-43"| What | Rule |
|---|---|
| Why | Without it, anyone who can write a ManagedWorkload, or a workload’s environment, in one namespace could keep another’s workloads awake |
| Not allowed | A dependency, declared or learned, isn’t held, woken or waited for. The dependent reports it without saying whether it exists |
| Never | Dependents never pause, scale down or delete a dependency. They only keep it awake or wake it |
| At most | So one in another namespace can only keep it running |
| Holders | HeldByDependents always names them by namespace |
A dependency that isn’t allowed reads:
DependencyNotAllowed=True not in a namespace that allows dependents from preview-42 with napwake.dev/dependents-from, so not held or woken: shared/statefulset/postgresLearned dependencies
Section titled “Learned dependencies”Napwake learns dependencies from each workload’s environment, and from the requests that wake it. Each is held and woken exactly like a dependsOn, with no YAML to write.
| From the environment | |
|---|---|
| Read | The environment of every workload Napwake manages, as kubectl napwake scan does |
| Counts | A variable, literal or from a ConfigMap, holding an address that names a Service |
| In any form cluster DNS gives | postgres, postgres.preview-42.svc.cluster.local, or a headless Service’s pod, postgres-0.postgres-hl |
| Followed to | The workload the Service selects |
| Read again | When the pod template changes, and hourly for ConfigMaps changed since |
| Never read | Secrets. An address set only in a Secret isn’t learned from the environment |
| Reported | A DependenciesLearned event when the set changes |
| The event says | Whether each is woken with the workload. Only one Napwake manages, in a namespace that allows it, is |
learnedDependencies: from: 3f9c2a71d0e4b815 at: "2026-10-04T09:12:00Z" dependencies: - {namespace: preview-42, kind: StatefulSet, name: postgres, source: environment, via: PGHOST, address: postgres-0.postgres-hl} - {namespace: preview-42, kind: StatefulSet, name: redis, source: wake}Opt the dependency in too, to have them paused and woken together:
api: depends on preview-42/postgres (PGHOST), which isn't managed by Napwake, so it isn't woken with api: opt it in to have them paused and woken togetherFrom wakes
Section titled “From wakes”A request that wakes a workload teaches which workload sent it.
| What | How |
|---|---|
| How | The doorman records where the request came from. The operator finds the pod that sent it |
| Learned | If Napwake manages that pod’s workload, it depends on the one it woke, with source: wake |
| Catches | Addresses in Secrets, built in code, or read from files, the first time they’re used |
| Teaches nothing | A request from a workload Napwake doesn’t manage, such as an ingress controller |
| Nor | A request from a pod on the node’s network, or a napwake.dev/last-request-from set by hand |
| Checked | Only an address the doorman wrote counts. The API server’s field managers must show Napwake’s own set it |
| Admission | The chart’s admission policy lets only the doorman set napwake.dev/last-request-from |
| Looked for in | The woken workload’s namespace, and those its napwake.dev/dependents-from allows |
| Kept | For 14 days after the last request that woke the dependency, through new reads of the environment |
lastSeen |
Records that request, at most once a day |
| While it’s kept | The dependency is held and woken with the workload |
| Renewed | Not while it’s kept, as the workload’s requests no longer reach the doorman |
| Dropped | Even in steady use, 14 days after it was learned or last renewed |
| Learned again | From the next request that wakes the dependency. That costs one cold start, about once a fortnight |
| One-off calls | Such as someone’s curl from a pod. Forgotten the same way, so they don’t tie two workloads together for good |
Dropping a learned link
Section titled “Dropping a learned link”A wrong learned link can only keep a workload awake longer, or wake it, never pause it. To drop one, list it in napwake.dev/ignore-dependencies on the workload or its ManagedWorkload. Separate entries with commas, as namespace/name or a name in its own namespace:
metadata: annotations: napwake.dev/ignore-dependencies: "redis, messaging/nats"dependsOn still applies alongside, and is the only way to set waitForReady. kubectl napwake deps shows a workload’s links both ways, and where each came from.
When you need dependsOn
Section titled “When you need dependsOn”Wake on request wakes a workload behind a normal Service by itself. Learned dependencies cover its environment, headless addresses included. dependsOn is for the rest:
waitForReady, so the first request doesn’t wait for the database too, or the app doesn’t fail starting before it.- Addresses not seen in use yet, from Secrets, code or files.
- A workload behind no Service, which no address names.
kubectl napwake scan lists the dependencies in each workload’s environment, and what Napwake does with each:
| When | What Napwake does |
|---|---|
| Napwake manages both | Woken and held with the workload that needs it, declared or learned |
| It doesn’t manage the dependency | Never paused |