Chapter 10 of 26
Patch Policies and Scheduling
A patch policy controls when an approved patch run actually fires on a host. Policies let you carve out maintenance windows, build in a delay for "I might change my mind" runs, or force an immediate execution on anything that matters. Assignments and exclusions let you apply a policy broadly (to a host group) while still carving specific hosts out of it.
This page walks through the policy model, how effective policies are resolved, the Settings UI, and how policies interact with run triggers.
Module Gate and Permissions
Patch policies are gated by the patching_policies capability module, which is separate from the base patching module. A deployment with patching enabled but not patching_policies can still trigger patch runs; they just run immediately and cannot be scheduled through a policy.
| Action | Required module | Required permission |
|---|---|---|
| View policies and their assignments | patching_policies |
can_view_hosts |
| Create, edit, delete policies | patching_policies |
can_manage_patching |
| Add or remove policy assignments (host / host group) | patching_policies |
can_manage_patching |
| Add or remove host exclusions | patching_policies |
can_manage_patching |
If patching_policies is not enabled, the Policies tab on the Patching page is shown with an "Upgrade required" placeholder and a tier badge.
Where Policies Live in the UI
There are two equivalent entry points, both showing the same policy list with the same editor:
- Patching page → Policies tab: the main view. Lists every policy, shows schedule type and assignment count, and lets you expand a policy to manage its assignments and exclusions inline.
- Settings → Patch Management: the admin-centric view, identical in function, kept so patch policies are discoverable alongside other operational settings.
Both pages are backed by the same /api/v1/patching/policies endpoints, so any change you make in one is visible immediately in the other.
The Policy Model
Each policy has the following fields:
| Field | Type | Description |
|---|---|---|
name |
string, required | Display name of the policy. |
description |
string, optional | Free-text description. |
patch_delay_type |
enum, required | immediate, delayed, or fixed_time. |
delay_minutes |
integer, required when delayed |
Minutes to wait after the trigger before running. |
fixed_time_utc |
string, required when fixed_time |
Time of day in HH:MM (or HH:MM:SS) format. Interpreted as local wall-clock time in the resolved organization timezone. The column name is retained for backward compatibility — see the timezone note below. |
timezone |
string, deprecated | Legacy IANA timezone field. No longer read by the scheduler and ignored on create/update. Persisted as NULL going forward. Existing values on old rows are kept for audit purposes only. |
The three delay types
Immediate. The run fires as soon as the asynq worker dequeues the task. This is the default policy behaviour when no policy is attached to a host. Use for development hosts or anything where you actively approve each run.
Delayed. The run is scheduled for now + delay_minutes at the moment it is triggered. Typical values are 30-60 minutes, enough time for an operator to cancel if the trigger was a mistake, but short enough that the patch still lands in the current shift. The delay is counted from the trigger time (or approval time, for patch_package), not from policy creation.
Fixed time. The run is scheduled for the next occurrence of HH:MM interpreted as local time in the organization timezone. If that time has already passed today (in the local zone), the run is scheduled for the same time on the next local calendar day. Use for maintenance windows (03:00 daily reboots, for example). Delays can be long: a run triggered at 14:00 for a 03:00 fixed-time policy will sit in scheduled status until the next 03:00 local.
Timezone handling
Fixed-time policies fire at the configured HH:MM interpreted as local wall-clock time in the organization timezone. The org timezone is resolved in this order:
TZenvironment variable on the server process.TIMEZONEenvironment variable on the server process.- Settings → General → Timezone (stored in the DB).
- Final fallback:
UTC.
The policy form shows the resolved zone next to the time input so operators can confirm which zone applies before saving. There is no per-policy timezone dropdown — scheduling is governed by a single org-wide timezone for consistency.
DST. During spring-forward, a policy time inside the missing hour is normalized one hour forward (e.g. 02:30 on the EU spring transition day fires at 03:30 local on that day). During fall-back, when a wall-clock time occurs twice, the standard-time (post-shift) occurrence is used. Review fixed-time policies in DST zones if a one-day shift on transition days would affect a maintenance window.
Column name. The DB column is still called fixed_time_utc for backward compatibility, but its contents are now local wall-clock time in the resolved timezone — not UTC. Renaming would require a multi-step migration; the name is preserved to avoid churn.
Breaking change in this release. Earlier versions parsed
fixed_time_utcas a literal UTC time and ignored the per-policy timezone dropdown. Existing fixed-time policies created under the old behavior will now fire at a different absolute instant — review and adjust them after upgrade. The per-policytimezonefield on the API is silently ignored on create/update going forward.
Creating a Policy
- Open Patching → Policies (or Settings → Patch Management).
- Click Create policy. A modal opens with the policy form.
- Fill in:
- Name: required. Pick something that describes the window, not the host set (e.g. "Nightly 03:00 UTC", not "Production web tier"). Host assignment is done separately.
- Description: optional but helpful.
- Patch delay:
Immediate,Delayed (run after N minutes), orFixed time (e.g. 3:00 AM).
- If you picked Delayed, enter the number of minutes (minimum 1).
- If you picked Fixed time:
- Enter the time in
HH:MMformat. The form displays the organization timezone next to the field; the time is interpreted as local wall-clock time in that zone (see the timezone handling note above). - There is no per-policy timezone dropdown. Change the org-wide zone under Settings → General → Timezone if you need a different default for scheduling.
- Enter the time in
- Click Create. The policy appears in the list with
0 assignment(s).
Policies are empty until you assign them. A newly-created policy is inert and does not automatically apply to any host.
Assigning Policies
A policy can be assigned to a host (direct) or a host group (indirect). Direct assignments take precedence over group assignments; see Effective Policy Resolution below.
To assign a policy:
- In the Policies list, click the N assignment(s) link on the policy row. The row expands to show the Applied to panel.
- Choose Host or Host group from the dropdown.
- Pick the target host or group from the second dropdown.
- Click Add.
The assignment takes effect immediately for any future patch runs on that target. Runs already queued against the old effective policy are not recomputed; they keep the policy snapshot from the moment they were triggered (visible in the Run Detail sidebar).
To remove an assignment, click the × next to its chip in the Applied to list.
Exclusions
Exclusions let you carve a specific host out of a policy that it would otherwise inherit through a host group. Direct host assignments cannot be excluded because the precedence rules make the direct assignment always win.
Typical use:
- You have a host group
production-webwith 50 hosts. - You assign a
Nightly 03:00 UTCpolicy to that group. - One particular host in the group (
prod-web-api-01) serves a customer in Singapore who cannot tolerate a 03:00 UTC outage (that's mid-day for them). - You add
prod-web-api-01as an Exclusion on the policy. That host is then treated as having no policy (falls back to Default / immediate) even though it is still in theproduction-webgroup. - Optionally, assign
prod-web-api-01directly to a different policy with a 19:00 UTC fixed time.
To add an exclusion, expand the policy and use the Exclusions row: pick a host from the dropdown, click Exclude host. The host is shown as an amber chip in the exclusions list.
Exclusions apply only to that specific policy. If the host is a member of another group assigned to a different policy, that other policy can still apply.
Effective Policy Resolution
When a patch run is triggered, the server resolves the effective policy for the target host using this precedence:
- Direct host assignment: if the host has any policy directly assigned, that policy wins. Exclusions do not apply here (you can't direct-assign and then exclude).
- Group assignment: if the host is a member of one or more host groups, the server walks the groups' policy assignments and picks the first policy (by assignment
created_atascending) where the host is not excluded. - Default: if none of the above applies, the effective policy is the implicit "Default" policy, which is equivalent to
patch_delay_type=immediate. The Run Detail sidebar shows this as "Default policy. Runs immediately on trigger."
If a host is in multiple groups with conflicting policies, the oldest policy assignment wins. Order matters. If you need deterministic behaviour in a complex fleet, prefer direct host assignments over layered group policies, or design your groups so that each host is only in one "scheduling" group.
Checking the effective policy
Before triggering a run, the Patch Wizard's Timing step calls GET /patching/preview-run?host_id=<id> for each selected host. The response contains the run_at_iso time (what ComputeRunAt returns right now, computed in the org timezone for fixed-time policies) and the resolved policy's name, ID, and delay type. That's how the wizard tells you "Runs at 03:00 (Europe/London) via Nightly-Window" before you click fire.
The policy snapshot
When a run is created, the server also takes a snapshot of the effective policy onto the run row (policy_snapshot JSON). The snapshot is what the Run Detail page displays, and it is immutable; changing or deleting the policy later does not rewrite the snapshot. This is important for audit: "which policy was in effect when this run fired on 12 March?" always has an answer, even if the policy has since been deleted.
For fixed-time policies the snapshot also records schedule_timezone: the IANA name actually resolved when run_at was computed. Run Detail prefers this field when displaying the schedule, so changing the org timezone after the fact does not retroactively rewrite the audit trail.
Scheduling Semantics
Once the effective policy is resolved, the server converts it into an asynq job delay:
| Policy type | delayMs computation |
Visible run status |
|---|---|---|
immediate |
0 |
queued immediately |
delayed |
delay_minutes × 60 × 1000 |
scheduled for run_at = now + delay_minutes |
fixed_time |
ms until next HH:MM in the org timezone |
scheduled for run_at = next HH:MM (local in the org zone, stored as UTC) |
The patch_runs row stores both created_at (when the run was inserted) and scheduled_at (when asynq should release it to the worker). The Runs & History table shows scheduled_at as "Started" time if the run has not yet started.
Schedule overrides at trigger / approve time
Both POST /patching/trigger and POST /patching/runs/{id}/approve accept a schedule_override field. The only currently-supported value is "immediate", which forces delayMs=0 regardless of the effective policy. This is what the Run immediately checkbox in the Patch Wizard Timing step sets.
The snapshot on the run is still taken from the effective policy. The override only changes the actual firing time, not the policy metadata. Run Detail will show the real policy (e.g. "Nightly 03:00 UTC") but the run's scheduled_at will be absent and its status will jump straight to queued.
Deleting a scheduled run
A scheduled run can be deleted from Runs & History. When you click Delete on a scheduled row:
- The server removes the run's row from the
patch_runstable. - It also calls
inspector.DeleteTask("patching", "patch-run-<id>")to remove the queued asynq task, so the run doesn't fire after being deleted.
Deletion is only allowed for runs in queued, pending_validation, pending_approval, validated, approved, or scheduled status. Anything running or terminal is not deletable (use Stop Run for a running run; terminal runs are historical records and cannot be removed from the UI).
Editing and Deleting Policies
Editing a policy in place (change name, description, delay type, or delay value) is supported from the Policies list. Click the pencil icon on a policy row to open the edit modal, then Update.
Existing runs are not re-scheduled when you edit a policy; their snapshot was taken at trigger time. Only future runs will use the new values.
Deleting a policy removes it immediately. All assignments and exclusions attached to it are removed with it (cascade delete). Any run in scheduled status that was created from this policy keeps its scheduled_at and still fires when the time comes. The policy ID on the run becomes a dangling reference, but the policy name is preserved in the policy_snapshot column for the UI.
If you're replacing a policy with a new one, prefer reassigning hosts and groups to the new policy before deleting the old one.
Common Patterns
Single maintenance window across the fleet
Create one policy (Nightly 03:00 UTC) with fixed_time at 03:00. Assign it to a top-level host group that contains everything, or to each host directly. Use exclusions for the handful of hosts that need a different window.
Canary-then-production
Create two policies:
Canary 01:00 UTCwithfixed_timeat01:00, assigned to yourcanaryhost group.Production 04:00 UTCwithfixed_timeat04:00, assigned to yourproduction-allhost group.
Trigger the same patch_package run on both groups at the same time. The canary hosts patch first; production follows three hours later. If canary reports failures, delete the still-scheduled production runs before they fire.
Slow-rollout "hold for 30 minutes"
Create a Delayed 30min policy with patch_delay_type=delayed, delay_minutes=30. Assign it to everything. Every approved run goes into scheduled status for 30 minutes before firing. If you realise you approved the wrong thing, delete the scheduled run; otherwise it fires automatically.
Mixed: immediate by default, fixed-window for production
- Leave most hosts with no assignment. They fall through to the Default (immediate) policy.
- Create a
Prod 03:00 UTCpolicy and assign it directly to theproductiongroup. - A patch triggered from the Packages page across the fleet then runs immediately on dev/staging and waits for the next 03:00 UTC on production.
Related Documentation
- Patching Overview: the three core concepts and how patching fits together.
- Running a Patch: the Patch Wizard flow, including the Timing step that reads the effective policy.
- Patch History and Live Logs: reading run history, including the policy snapshot shown on each run.
- Hosts and Groups: managing host groups, which are the usual unit of policy assignment.