---
title: "Drift detection"
description: "Check a workspace on a schedule for infrastructure changes made outside StackShip, get an email when it drifts, and revert the drift with one approved run."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.stackship.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Drift detection

Drift is a difference between your real infrastructure and the workspace's
state: someone changed a resource in the cloud console, a script edited a
tag, or a resource was deleted by hand. Drift detection checks for it on a
schedule, so you find out without anyone pushing a commit.

## What a check compares

A drift check is a refresh-only plan. It reads every resource in the
workspace's state from your cloud provider and compares what it finds with
what the state says.

- It doesn't compare your configuration with your infrastructure, so a newer
  module version, a data source such as "latest AMI", or `timestamp()` never
  shows up as drift.
- It never applies, never writes state, and never locks state.
- It never posts a GitHub check or pull request comment.

The check appears in the run list with the kind **Drift check**. Its run page
shows each resource that drifted, with the attribute-level differences, in
the same plan view as any other run. A check that finishes reads **Plan
completed**, whether or not it found drift.

## Turn on drift detection

1. Open the workspace and select the **Drift** tab.
2. Choose a **Frequency**. For **Daily** and **Weekly**, choose the **Hour
   (UTC)**. For **Weekly**, also choose the **Weekday**.
3. Select **Save**.

When you first turn drift detection on, the first check starts within about
5 minutes. The tab then shows when the next check is due.

Saving the same settings again doesn't move the next check. Changing the
frequency, hour, or weekday moves the next check to the new time; it doesn't
start one right away.

To stop checking, select **Turn off**. The workspace's drift status is hidden
until you turn drift detection on again. Past drift checks stay in the run
history.

## Frequency and timing

| Frequency | When checks run (UTC) |
| --- | --- |
| Every 6 hours | 00:00, 06:00, 12:00, and 18:00 |
| Every 12 hours | 00:00 and 12:00 |
| Daily | The hour you choose |
| Weekly | The hour you choose, on the weekday you choose |

All times are UTC. Each workspace adds its own fixed minute, from 0 to 59, so
a check starts up to 59 minutes after the hour shown, at the same minute every
time. This spreads checks across your organization rather than starting them
together. The most frequent schedule is every 6 hours. A new schedule
defaults to **Daily**.

## What a check uses

**Configuration.** A check uses the configuration of the workspace's latest
successful plan-and-apply run. If the workspace has never applied through
StackShip (for example, you moved its state in with `tofu state push`), it
uses the configuration of its latest plan that finished. Speculative plans
from pull requests are never used. Until the workspace has one of these, a
scheduled check is skipped and waits for its next scheduled time.

**Variables and credentials.** A check gets the same workspace variables,
secrets, and cloud credentials as a plan-only run in the workspace. If the
workspace has no cloud credentials for plans, every check fails; see
[cloud access](/workspaces/variables).

## Checks never block your runs

A drift check doesn't count toward the workspace's or organization's active
runs, doesn't lock state, and doesn't hold the workspace. A push, a `tofu`
CLI run, or an API run starts exactly as it would with no check running.

When the workspace is busy with another run, StackShip skips the scheduled
check and tries again about 15 minutes later, rather than making your run
wait. Only a limited number of drift checks run at once, both in your
organization and in each execution region; a check over either limit also
tries again about 15 minutes later.

## Drift status

While drift detection is on, the workspace shows a drift status on its
overview and in the workspace list.

| Status | Meaning |
| --- | --- |
| **No drift** | The latest check found no differences, or the current state was written by a StackShip apply, which reads real infrastructure as it runs |
| **Drift detected** | The latest check found differences. The badge shows how many resources would be added, changed, and destroyed to bring state back in line, such as `+0 ~2 −1` |
| **Check failed** | The latest check stopped before its plan finished, for example because the workspace has no cloud credentials. Open the check to read the error |
| **Checking** | A check is running |
| **Not checked yet** | No check has finished against the current state yet |

A check only counts against the state it read. When an apply or a
`tofu state push` writes new state, the status starts over: **No drift**
after a StackShip apply, **Not checked yet** after a state push, until the
next check finishes.

### Why a check didn't run

When a scheduled check is skipped, **Not checked yet** and the **Drift** tab
show why:

| Reason | What it means |
| --- | --- |
| Runs once the workspace has a plan or apply | There's no configuration to check yet. The check waits for its next scheduled time. Push to the tracked branch or run `tofu plan` |
| Waiting for the active run to finish | Another run, state upload, or recovery is in progress. The check tries again in about 15 minutes |
| Waiting for capacity | Your organization, or the workspace's execution region, is already running as many drift checks as it can at once. The check tries again in about 15 minutes |
| Runs are paused | StackShip isn't starting new runs right now. The check tries again in about 15 minutes |
| Cloud credentials need attention | A [credential profile](/workspaces/variables#cloud-credentials) on this workspace isn't verified, or isn't allowed to plan. Verify the profile or allow it to plan; the check tries again at its next scheduled time |

## Email when drift appears

When a workspace drifts, StackShip emails every owner and admin in your
organization with a verified email address. Deactivated users get no email.
The subject is **Drift detected in** followed by the workspace name. The email
says how many resources changed and has a **Review drift** link to the check.
It never includes resource addresses or attribute values.

Each person gets one email per drifted state. While the workspace stays
drifted against the same state, later checks send nothing. Once an apply or
state push changes the state, new drift sends a new email. There's no way to
turn these emails off yet.

## Revert drift

When the status is **Drift detected**, the workspace overview shows **Revert
drift**. It starts a normal plan-and-apply run from the same configuration
the check used (not the latest commit on the tracked branch) and opens it.

The plan shows what the apply would change to undo the drift. Review it, and
approve it like any other run; see [run approvals](/runs/approvals). If the
workspace is busy with another run, **Revert drift** shows why the run
couldn't start.

To keep a change someone made outside StackShip instead, update your
configuration to match and push it.

## Permissions

| Role | Can do |
| --- | --- |
| Member | See drift status and the **Drift** tab settings; select **Revert drift** if they can start runs |
| Admin or owner | Everything a member can, plus turn drift detection on, change its schedule, and turn it off. Approve the revert run |

Changes to a schedule are recorded in the [audit log](/governance/audit) as
`workspace.schedule.updated` and `workspace.schedule.deleted`.

## Manage drift detection with the API

| Request | Description |
| --- | --- |
| `GET /v1/workspaces/{workspace_id}/schedule` | Read the schedule, with `next_run_at` and `last_skip_reason`. Returns `404` when drift detection is off |
| `PUT /v1/workspaces/{workspace_id}/schedule` | Turn drift detection on or change it. Send `cadence` (`every_6_hours`, `every_12_hours`, `daily`, or `weekly`), `hour_utc` (0–23, for `daily` and `weekly`), and `weekday` (0–6, Sunday is 0, for `weekly`). Leave the others `null`. Requires an `Idempotency-Key` |
| `DELETE /v1/workspaces/{workspace_id}/schedule` | Turn drift detection off. Requires an `Idempotency-Key` |
| `GET /v1/runs?kind=drift_check` | List drift checks. An unknown kind returns `400 RUN_LIST_KIND_INVALID` |

```bash
curl -X PUT https://api.stackship.run/v1/workspaces/ws_9x2p/schedule \
  -H "Authorization: Bearer $STACKSHIP_TOKEN" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"cadence": "daily", "hour_utc": 6, "weekday": null}'
```

Every workspace object includes `drift`: the `status`, `checked_at`,
`run_id` of the check, its `configuration_version_id`, the `add`, `change`,
and `destroy` counts, and `skip_reason`. It's `null` while drift detection is
off. To revert drift through the API, send `POST /v1/runs` with
`kind: "plan_and_apply"` and that `configuration_version_id`.

You can't start a drift check with `POST /v1/runs`; only a schedule starts
them. AI agents can read and set schedules too; see
[MCP server](/automation/mcp).

## Next steps

- [Runs](/runs)
- [Run approvals](/runs/approvals)
- [Variables and secrets](/workspaces/variables)

Source: https://docs.stackship.run/workspaces/drift/index.mdx
