Skip to content
Docs
Release candidateThese docs are for Napwake 0.2.0-rc.9, a release candidate, and may change before it's released. Read the docs for the latest release.

Guides

Pause and resume

Napwake's one action is to pause a workload, scaling it to zero replicas. Only the pods go. The Deployment or StatefulSet, its PVCs and their data stay.

View .mdAsk Claude (opens claude.ai)Ask ChatGPT (opens chatgpt.com)
  1. Awake · running

    Pods running

  2. No activity for idleAfter, or kubectl napwake pause
  3. Asleep · paused

    Scaled to zero

    Its PVCs and their data stay.

  4. A request, activity, autoResume or kubectl napwake wake
  5. Waking up · resuming

    Scaled back up

    To the replicas it had, within its HPA's or ScaledObject's range.

Its replicas are Ready

Napwake never deletes a workload or its storage. A StatefulSet whose persistentVolumeClaimRetentionPolicy.whenScaled is Delete has its own controller delete its PVCs as it scales down. So Napwake never pauses one. It reports Protected=True, reason PVCsDeletedOnScaleDown, until whenScaled is Retain.

When you’re done with a workload for the day, such as a preview environment, pause it without waiting for idleAfter.

  1. Ask for the pause.

    Terminal window
    kubectl napwake pause my-api -n staging

    Name the workload as kubectl napwake status shows it. It needs no idlePolicy, and works on a ManagedWorkload from the label too.

  2. Read the answer in the PauseRequest condition.

    Terminal window
    kubectl describe managedworkload my-api -n staging

    Its message says what happened and when, in UTC. It reads paused as requested at 2026-10-21 03:32 UTC, already paused at …, or not paused at 2026-10-21 03:32 UTC: and why. It answers the last request until the next one, through any wakes and pauses, so it outlives the events.

The workload is Paused, the same as after an idle pause. Anything in Resume wakes it.

The request runs the idle clock out now, and the pause follows the rules of an idle one:

The workload What Napwake does PauseRequest
Running, Idle or Resuming Pauses it, after the wake if it’s Resuming, with a PauseRequested event, then Paused True, Paused
A request comes as it pauses Calls the pause off, and says so. kubectl napwake pause says it wasn’t paused False, PauseCancelled
Pausing or Paused Finishes any pause under way, then marks the request handled True, AlreadyPaused
In the hour Napwake waits after a GitOps tool undid its last pause Pauses it, since you asked. If the tool undoes it again, that’s a new GitOpsConflict True, Paused
Busy within the hour, by a confident forecast Leaves it running and says when, since it would likely be woken straight back. kubectl napwake pause asks whether to pause anyway, and --yes does False, ForecastExpectsDemand
In dry-run Never scales it, and counts a would-be pause False, DryRun
Held awake by napwake.dev/active-until on it or its workload, depended on by awake workloads, in a dependsOn cycle, in a protected namespace, or Napwake’s own operator or doorman Leaves it running, with a Warning event, or HeldByDependents for dependents False, ActiveUntil, HeldByDependents, DependencyCycle or Protected
Scaled to zero outside Napwake Nothing, since it’s off already False, ScaledToZero
Missing, labelled napwake.dev/ignore, or managed by another ManagedWorkload Nothing, with a Warning event False, TargetNotFound, TargetIgnored or DuplicateTarget
Already on the ManagedWorkload as Napwake takes it on, or takes a pause up again after its status was lost Nothing. It may have been answered before, and brought back with the ManagedWorkload, as by a backup restored without status. Set a new value to ask False, PredatesStatus

A request Napwake won’t act on is answered once, so ask again once the reason is gone. Napwake keeps its record of answered requests in the ManagedWorkload’s status. So a request already on it as Napwake takes it on counts as answered.

Otherwise a backup restored without status would bring back requests answered long ago. So would kubectl replace --force, or an old manifest applied again. Set the annotation once the ManagedWorkload is Running.

Activity from before the request doesn’t undo it. Only an activity annotation changed since wakes it. A query already active at the pause, such as a queue with messages, wakes it once it has been quiet.

Ask from a script

Set napwake.dev/pause-requested on the ManagedWorkload, as kubectl napwake pause does. Any value that differs from the last request’s works, such as the time:

Terminal window
kubectl annotate managedworkload my-api -n staging --overwrite \
napwake.dev/pause-requested="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

Napwake acts on each value once, recording it in status.lastPauseRequest, which survives an operator restart. napwake.dev/pause-overrides-forecast overrides the forecast for one request.

Before the pods stop, the doorman steps in front of the workload’s Services, beside its pods. So no request is refused as they stop.

When What happens
The move to Pausing The doorman’s routes go into status.doorman, and every doorman pod listens on them. status.pause.routesWrittenAt records when they were stored
5 seconds after, once every doorman pod answers on those ports The operator checks that by connecting to each. The doorman’s EndpointSlice then stands beside the pods’ own. The doorman passes what it gets straight to a Ready pod. status.pause.routedAt records when
5 seconds after that kube-proxy and ingress controllers send to it too. The pause goes on, however late Napwake gets to it
A workload the doorman doesn’t route The pause goes on at once
At most None waits more than a minute for the doorman

Then Napwake scales the workload to zero. Lifecycle has what it records before it scales anything.

Kubernetes stops the pods as it stops any pod. Each gets SIGTERM, then its terminationGracePeriodSeconds (30 seconds by default) to finish before SIGKILL. Napwake waits for them to stop (how long). Requests that come meanwhile go to the doorman, which holds them.

An app that should finish a job or drain connections needs to handle SIGTERM, as in a rollout. One that exits the moment it gets SIGTERM can refuse a connection. kube-proxy may send it one in the second before it drops the pod. A preStop sleep of a few seconds closes that gap, in a pause as in any rollout.

A request while a workload pauses shows it’s in use. Whatever would wake it once paused calls the pause off, if it comes while it’s Pausing:

  • a request that reaches the doorman, which holds it, or passes it on to a pod that’s still Ready
  • for a workload that asks before waking, only a person’s click on the asleep page
  • a wake request
  • an activity annotation, which a waking dependent stamps

The workload is Running again, scaled back up if its pods had begun to stop. The request is answered, held while no pod is Ready. A PauseCancelled event says what came:

a request came as it paused; the pause is cancelled

The doorman doesn’t see every request as a workload pauses. The pods answer these, and the pause goes on:

  • requests in the first 5 seconds or more, before the doorman’s EndpointSlice stands beside the pods’ own
  • what kube-proxy and ingress controllers still send to the pods, for about 5 seconds more, until they’re scaled down
  • requests on connections opened before the pause
  • every request after the first on a connection the doorman passed on
  • traffic from a GKE load balancer through NEGs, which goes to the pods’ own addresses

A probe, a scrape, a hunt for hidden files, or a TCP check that sends nothing doesn’t call it off. Dry-run never pauses, so there’s nothing to call off.

A pause the idle clock made, called off this way, counts as made in use. The clock then waits longer before the next.

What happens The paused workload
A request reaches its Service Wakes, through wake on request
An activity annotation, napwake.dev/last-activity, napwake.dev/last-request or napwake.dev/active-until, changes on the ManagedWorkload or its target Wakes, whatever time it states
A new napwake.dev/wake-requested value on the ManagedWorkload, as kubectl napwake wake sets it Wakes, whatever the wake setting, with a WakeRequested event (Wake now)
A Prometheus activity query shows activity, such as messages waiting for a queue worker Wakes
autoResume: true, and a confident forecast expects demand in the coming hour Wakes 15 minutes before the hour
Something else scales it up, such as kubectl scale Counts it as activity, and goes straight to Running. For a GitOps tool, see GitOps tools
Dry-run is turned on Wakes, with a DryRunWake event. Dry-run never leaves a workload paused

Napwake looks at a paused workload at least every 5 minutes, or every minute with Prometheus activity queries. It also looks 15 minutes before each hour, on the hour, and whenever the workload or its target changes.

Terminal window
kubectl napwake wake my-api -n staging

wake asks Napwake to wake the workload now, whatever its wake setting. So it wakes one that asks before waking too. A paused workload wakes with its dependencies, and an awake one’s idle clock restarts. --for 2h keeps it awake at least that long, as for a demo. wake waits for Napwake’s answer, then until it’s Running.

Any tool can ask the same way, such as a pull request bot, a developer portal or a chat command. It sets napwake.dev/wake-requested on the ManagedWorkload, as wake does. The value only has to differ from the last request’s, such as the time or a CI run’s ID:

Terminal window
kubectl annotate managedworkload my-api -n staging --overwrite \
napwake.dev/wake-requested="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

A ManagedWorkload from the label is named after its workload. When that name is taken, it adds the kind, such as my-api-statefulset. It’s an annotation, like pause-requested, so a GitOps tool that owns the ManagedWorkload’s spec leaves it alone.

To It takes, on managedworkloads in the namespace
Set it with kubectl annotate get, patch
Set it from an HTTP client patch
Wait for the answer with kubectl wait get, list, watch
Run kubectl napwake wake get, list, patch

The built-in edit and admin roles include all of them once Napwake is installed. Public preview environments gives a minimal Role.

Napwake acts on each value once, recording it in status.lastWakeRequest, which survives an operator restart. The WakeRequest condition says what came of it:

The workload What Napwake does WakeRequest
Paused Wakes it, with a WakeRequested event, then Resumed once it’s Ready True, Woken
Pausing Calls the pause off, and has it Running again True, Woken
Resuming Lets the wake finish, then marks the request handled True, AlreadyAwake
Running or Idle Restarts its idle clock, as activity does True, AlreadyAwake
In dry-run Restarts its idle clock, which ends a would-be pause True, AlreadyAwake
Scaled to zero outside Napwake Leaves it off. Napwake only wakes what it paused False, ScaledToZero
In a protected namespace, Napwake’s own operator or doorman, or a StatefulSet that deletes its PVCs when scaled down Nothing, since Napwake never pauses it False, Protected
Missing, labelled napwake.dev/ignore, or managed by another ManagedWorkload Nothing, with a warning event False, TargetNotFound, TargetIgnored or DuplicateTarget
Its ManagedWorkload is being deleted Hands the workload back, with the replicas it had False, BeingDeleted
Set before Napwake has taken the ManagedWorkload on, as by a tool that creates it and asks at once Takes it on Running, so it’s awake, and restarts its idle clock True, AlreadyAwake
Already on the ManagedWorkload as Napwake takes a pause up again after its status was lost Nothing. It may have been answered before, as with a backup restored without status, so the workload stays paused. Set a new value to ask False, PredatesStatus

The condition answers the last request until the next one, through every pause and wake since. Its message says what happened and when, in UTC. It reads woken as requested at 2026-10-23 09:05 UTC, already awake at …, or not woken at 2026-10-23 09:05 UTC: and why.

Terminal window
kubectl get managedworkload my-api -n staging \
-o jsonpath='{.status.conditions[?(@.type=="WakeRequest")]}'

A request Napwake can’t act on is answered once, not retried. Ask again once the reason is gone.

Unlike a pause request, a wake request may be set any time, even before Napwake has taken the ManagedWorkload on. At worst an old one restarts the idle clock.

A resume wakes the workload’s dependencies, then scales it back to status.pause.previousReplicas. It’s Running once those replicas are Ready, by a status its own controller wrote after the scale-up. Once it’s scaled back up, its autoscaler owns the replicas. An HPA scaling it down as it starts isn’t undone.

A workload with a pod crash-looping before its pause would never get there. So Napwake stops waiting, runs it with its Ready replicas, and sets ResumeIncomplete=True. With none Ready, its doorman routes go anyway. Requests then fail until a pod is Ready, rather than wait at the doorman for a workload its own controller reports stuck. A StatefulSet with the default OrderedReady pod management starts its pods one at a time. A slow-starting one may have only its first pods Ready when the 2 minutes are up. It runs with those, and the rest follow. Lifecycle has the timings.

When Napwake stops managing a paused workload, it hands it back. It scales it to the replicas it had and releases it from KEDA, without waiting for Ready. Its Services stop routing to the doorman. A Resumed event says why, and how many replicas it has. That happens when:

  • The ManagedWorkload is deleted, or the napwake.dev/managed label that created it is removed.
  • The workload is labelled napwake.dev/ignore: "true".
  • Its namespace becomes protected.
  • It’s a StatefulSet that deletes its PVCs when scaled down.
  • It’s Napwake’s own operator or doorman, the Deployments labelled control-plane: controller-manager or doorman in Napwake’s namespace. Those are never paused, whatever the namespace allows.
  • An older ManagedWorkload for the same workload takes it over, and this one becomes the DuplicateTarget.

So no longer managing a workload never leaves it switched off. The napwake.dev/cleanup finalizer makes sure of it when a ManagedWorkload is deleted, retrying a failed hand-back. In a namespace being deleted, the workload goes too. So after 5 minutes the ManagedWorkload lets go with a HandBackAbandoned Warning event.

If the target itself is deleted while paused, its Services stop routing to the doorman. Nothing would start the pods. TargetAvailable turns False, reason TargetNotFound.

A workload scaled to zero outside Napwake is off on purpose. That’s by kubectl scale, a pipeline, or KEDA with no active trigger. Napwake leaves it off, so scaling to zero yourself keeps a workload off until someone starts it.

When What happens
Its idle clock runs out, or you ask for a pause Nothing. A pause request gets ScaledToZero, since a pause would record zero replicas to restore
A request, an activity annotation, autoResume or kubectl napwake wake Nothing wakes it. Napwake only wakes what it paused
A request to one of its Services It fails, as without Napwake. Its Services aren’t routed to the doorman, so nothing waits for pods nothing will start

The ManagedWorkload stays Running, with the ScaledToZero condition and one ScaledToZero event. Once whoever scaled it down sets its replicas above zero, Napwake manages it again. That counts as activity, so its idle clock starts afresh.

Napwake never scales up a workload it didn’t scale down. A pause recorded at zero replicas is handed back at zero, with a Resumed event saying so.

A paused workload costs only its storage. Cost tracking, always on, counts the rest:

While paused Accrues as
What its replicas requested in CPU and memory Freed
The part of that its nodes’ spare capacity couldn’t hold Saved
Its PVCs, since they stay Cost

To remove one nobody comes back to, delete it the way it was created. That’s from Git, by the pipeline that made it, or with a TTL tool such as kube-janitor. Those know when an environment is finished. Napwake only knows it’s idle.

Napwake records what a pause changes before it scales anything. A pause interrupted by a restart is then finished or undone from the record. Lifecycle has how.