Safety Monitoring
Safety monitoring checks each active feature flag's error rate and latency against thresholds you
set. When a flag breaches one and automatic rollbacks are turned on, it rolls the flag back. Its
endpoints are under /api/v1/safety.
What Safety Monitoring Does
The safety monitor runs every 5 minutes by default (SAFETY_CHECK_INTERVAL_MINUTES). For every
ACTIVE flag, whatever its rollout_percentage (a flag at 0% still serves the users its targeting
rules match), it:
- Loads the flag's safety configuration (or the platform default when the flag has none) and skips the flag
if monitoring is not
enabled. - Reads the current value of each configured metric over the last 15 minutes: error metrics come from
error_logs(relative to the flag's evaluations inraw_metrics), latency metrics fromraw_metrics. - Compares each value with the metric's
critical_thresholdusing itscomparison_type. A breach marks the flag unhealthy; awarning_thresholdbreach is only reported. - If the flag is unhealthy and the global setting
enable_automatic_rollbacksis on, rolls the flag back to the configuration'srollback_percentage. A target of0(the default) turns the flag off (status: inactive) and sets its percentage to 0; any other target lowers only the global percentage (see What a rollback changes). It also pauses the flag's active rollout schedules, records aSafetyRollbackRecord, and sends the Slack alert and webhook event.
You can run the same check on demand with GET /api/v1/safety/feature-flags/{flag_id}/check.
Which errors count
A flag's error metrics, and so automatic rollback, count two kinds of error: flag evaluations that
fail on the server, and error reports sent to POST /api/v1/tracking/errors or /errors/batch with
a server-side key, an API key with the sdk:ruleset scope whose owner can change feature flags
(ADMIN, DEVELOPER or a superuser). Error reports sent with any other key, such as the key in a
browser or mobile app, are stored and counted separately in the check
(details.uncounted_error_count), but do not count toward the error rate or a rollback. The same
goes for an sdk:ruleset key whose owner has since lost the role. To have app errors count, send
them to your server and forward them from there with a server-side key; see
Reporting client-side errors.
GET /api/v1/metrics/summary counts errors by the same rule: its error_rate leaves out the
reports that do not count, and its uncounted_error_count is the number of them in its period.
That error_rate still differs from the check's in three ways: it covers start_date to
end_date (by default the 30 days before now), not the last 15 minutes; it divides by the
aggregated flag evaluations (of its period, by default day), not the raw ones; and it is a
percentage, not a fraction, which can exceed 100 when errors outnumber those evaluations.
Metrics You Can Monitor
| Metric name | Source | Meaning |
|---|---|---|
error_rate |
error_logs ÷ flag evaluations |
Fraction of evaluations that logged an error that counts (0.0–1.0; see Which errors count) |
error_count |
error_logs |
Number of errors that count, in the window |
total_evaluations |
raw_metrics |
Number of flag evaluations in the window |
latency, avg_latency |
raw_metrics (metric_type = latency) |
Average latency in ms |
p95_latency |
raw_metrics |
95th percentile latency in ms |
max_latency, min_latency |
raw_metrics |
Extremes in ms |
Each metric takes a MetricThreshold:
| Field | Type | Description |
|---|---|---|
critical_threshold |
float | Breaching it makes the flag unhealthy (rollback candidate) |
warning_threshold |
float | Reported in the check details; never triggers a rollback |
comparison_type |
"greater_than" (default), "less_than", "equal_to" |
How the current value is compared with the thresholds |
Metric names outside this list are accepted but have no data source; they are reported as healthy.
Configuring Per-Flag Safety
Run the commands on the rest of this page in one terminal, in order, against the stack from the Quick Start. Each uses the shell variables set by the ones before it.
Log in and create a flag
Changing safety settings takes a superuser, such as the demo administrator:
TOKEN=$(curl -s -X POST localhost:8000/api/v1/auth/login \
-H 'content-type: application/json' \
-d '{"email":"admin@demo.com","password":"Demo1234!"}' | jq -r .access_token)
curl -s localhost:8000/api/v1/auth/me -H "Authorization: Bearer $TOKEN" | jq '{is_superuser}'
It prints "is_superuser": true.
The examples watch a flag that is on for half of all users. This creates it and saves its id
in $FLAG_ID (see Creating Feature Flags):
FLAG_ID=$(curl -s -X POST localhost:8000/api/v1/feature-flags/ \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"key": "new-checkout-flow", "name": "New Checkout Flow", "rollout_percentage": 50, "is_active": true}' \
| jq -r .id)
curl -s localhost:8000/api/v1/feature-flags/$FLAG_ID \
-H "Authorization: Bearer $TOKEN" | jq '{status, rollout_percentage}'
It prints "status": "active" and "rollout_percentage": 50.
A flag without a configuration
GET /api/v1/safety/feature-flags/{flag_id}/config never answers 404 for a flag without a
configuration. It returns a default built from the global settings, with the nil UUID as
id. Nothing is written until you POST:
curl -s localhost:8000/api/v1/safety/feature-flags/$FLAG_ID/config \
-H "Authorization: Bearer $TOKEN" | jq '{id, enabled}'
Create or Update a Flag's Configuration
Requires a superuser. The call creates the configuration, or replaces the fields you send:
curl -s -X POST localhost:8000/api/v1/safety/feature-flags/$FLAG_ID/config \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"enabled": true,
"metrics": {
"error_rate": {"warning_threshold": 0.02, "critical_threshold": 0.05, "comparison_type": "greater_than"},
"p95_latency": {"warning_threshold": 300, "critical_threshold": 500, "comparison_type": "greater_than"}
},
"rollback_percentage": 0
}' | jq -c '.metrics'
It prints the stored thresholds:
{"error_rate":{"warning_threshold":0.02,"critical_threshold":0.05,"comparison_type":"greater_than"},"p95_latency":{"warning_threshold":300.0,"critical_threshold":500.0,"comparison_type":"greater_than"}}
Request Fields
| Field | Type | Required | Description |
|---|---|---|---|
enabled |
boolean | No | Whether the monitor checks this flag (default true) |
metrics |
object | No | Map of metric name → threshold (see above). An empty map means nothing is checked |
rollback_percentage |
int | No | Target of the automatic rollback, 0–100 (default 0); any other value answers 422. A value stored before that bound is clamped to 0–100 when the monitor uses it. 0 turns the flag off for every user, including users matched by a targeting rule. A value from 1 to 100 lowers the global rollout, for example 5 to keep a small slice of users on the flag; users matched by a targeting rule keep their rule's percentage (see What a rollback changes). Manual rollbacks take the percentage as a query parameter |
A field you leave out keeps its stored value; on the first POST it takes its default. No
field accepts null: a null answers 422 with "type": "null_not_allowed" and nothing is
written. A configuration saved with "metrics": null by an earlier version reads back with
"metrics": {}, so nothing is checked for that flag until you POST its thresholds again.
The response (the same shape as GET .../config) also carries the configuration's id,
feature_flag_id, enabled, rollback_percentage, created_at and updated_at.
Example: Checkout Flag with Tight Thresholds
{
"enabled": true,
"metrics": {
"error_rate": {"critical_threshold": 0.02, "comparison_type": "greater_than"},
"p95_latency": {"critical_threshold": 300, "comparison_type": "greater_than"}
}
}
Example: Non-Critical UI Flag, Alerts Only
Set only warning_thresholds (no critical_threshold): the check reports warnings but the flag never becomes
unhealthy, so it is never rolled back.
{
"enabled": true,
"metrics": {
"error_rate": {"warning_threshold": 0.10, "comparison_type": "greater_than"}
}
}
Global Safety Settings
Global settings hold the automatic-rollback switch and the default metric thresholds applied to flags that have no configuration of their own.
View Global Settings
Any logged-in user can read them:
curl -s localhost:8000/api/v1/safety/settings \
-H "Authorization: Bearer $TOKEN" | jq '{enable_automatic_rollbacks}'
It prints "enable_automatic_rollbacks": true: the Quick Start's demo data turns automatic
rollbacks on. The response also
carries default_metrics, id, created_at and updated_at.
On a database where no settings have been stored, the first read creates them with
enable_automatic_rollbacks: false and no defaults. Automatic rollbacks are then off until
a superuser turns them on.
Update Global Settings
Requires a superuser:
curl -s -X POST localhost:8000/api/v1/safety/settings \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{
"enable_automatic_rollbacks": true,
"default_metrics": {
"error_rate": {"warning_threshold": 0.05, "critical_threshold": 0.10, "comparison_type": "greater_than"}
}
}' | jq -c .default_metrics
It prints the stored defaults:
A flag with a configuration of its own is checked against its own thresholds only; the defaults apply to a flag that has none.
Running a Check
curl -s localhost:8000/api/v1/safety/feature-flags/$FLAG_ID/check \
-H "Authorization: Bearer $TOKEN" \
| jq '{is_healthy, metrics: [.metrics[] | {name, current_value, threshold, is_healthy}]}'
The new flag has no traffic, so every value is 0.0 and the flag is healthy:
{
"is_healthy": true,
"metrics": [
{
"name": "error_rate",
"current_value": 0.0,
"threshold": 0.05,
"is_healthy": true
},
{
"name": "p95_latency",
"current_value": 0.0,
"threshold": 500.0,
"is_healthy": true
}
]
}
Each metric's details holds its warning_threshold, critical_threshold, comparison_type,
warning (whether the warning threshold is breached) and measured (false for a metric name
with no data source). The response also carries
feature_flag_id, last_checked and details, with the window's error_count,
total_evaluations and uncounted_error_count: the error reports that were stored but do not
count (see Which errors count). When a metric breaches its critical
threshold, that metric and the whole response read "is_healthy": false.
The dashboard shows the same check. The flag's page has a Safety check section that reads Healthy or Unhealthy (or Error rate unknown when errors were reported but no evaluations fall in the last 15 minutes), with each metric's value beside its threshold, and a line saying how many error reports in the window were sent with keys that do not count. Admin → Safety (for superusers) lists the first 100 flags under Flag Safety Status as healthy, warning or critical, with its error rate and how many reports were not counted.
Auto-Rollback
When the monitor finds a flag unhealthy and enable_automatic_rollbacks is on, it:
- Locks the flag row and rolls the flag back to its configured
rollback_percentage(default0, which turns the flag off), pausing the flag's active rollout schedules in the same transaction - Records a
SafetyRollbackRecordwith trigger typeautomatic, the previous and target percentages, and the reason (trigger_reason, the same text as the audit entry'sreasonbelow: the record has no columns of its own for the metric value or the threshold) - Writes a
safety_rollbackentry to the flag's audit history (GET /api/v1/audit-logs/entity/feature_flag/{flag_id}), withsystem:safety-monitoras itsuser_emailand nouser_id. Itsreasonis the rollback's: the first breaching metric with its value and threshold, in the metric's unit and each to four significant figures (an error rate as a percentage, so 1 error in 14 evaluations reads7.143%; a latency in milliseconds), worded by the threshold'scomparison_type:aboveand>forgreater_than,belowand<forless_than,equal toand=forequal_to. Such asAutomatic rollback due to error_rate above threshold (10% > 5%), or, for aless_thanthreshold of 100 ontotal_evaluationsand a flag evaluated 40 times,Automatic rollback due to total_evaluations below threshold (40 < 100). Itsnew_valueholdstrigger_type(automatic),previous_percentage,new_percentage,deactivatedandpaused_schedules(see Audit Logging) - Sends a Slack alert to
SLACK_DEFAULT_CHANNELand asafety_rollbackevent toNOTIFICATION_WEBHOOK_URL, each when configured. Both name the first breaching metric with its measured value and threshold in the metric's unit: an error rate as a percentage (1 error in 14 evaluations against0.05readserror rate 7.143% above threshold 5%), a latency in milliseconds (avg latency 523.5 ms above threshold 500 ms). The line saysabove,beloworequal toby the threshold'scomparison_type, as the reason does. The webhook'smetadatacarries the raw numbers asmetric,valueandthreshold, anderror_rateholds the value only when the metric iserror_rate(otherwisenull). An automatic rollback sends no email
Investigate the root cause before re-enabling.
What a rollback changes
A rollback, automatic or manual, does one of two things, depending on its target:
- A rollback to
0%turns the flag off. Its status becomesinactiveand its globalrollout_percentagebecomes 0. Every user, including users matched by a targeting rule, getsenabled: falsewithreason: "inactive", exactly as when the flag is turned off by hand (see Creating Feature Flags). The targeting rules are left as written. - A rollback to 1–100% lowers only the global
rollout_percentage. A user who matches no targeting rule is bucketed by the new percentage. A user who matches a rule is bucketed by that rule's ownrollout_percentage(100 unless the rule sets one), so the rollback does not change whether that user gets the flag.
Either way, the flag's active rollout schedules are paused, so a later stage cannot raise the percentage again; they stay paused until you resume them.
So the flag's default value is served to every user after a rollback to 0%, and after a partial
rollback to the users it removes, while users matched by a targeting rule keep their rule's
percentage.
A partial rollback does not help when the harm is only among users matched by a targeting rule,
and a flag that serves only rule-matched users (global 0%) is never rolled back to a partial
target; use a target of 0 for such a flag.
What Triggers a Rollback
| Condition | Trigger |
|---|---|
Any metric's current value breaches its critical_threshold |
Automatic rollback (first breaching metric is named in the reason) |
Only warning_thresholds are breached |
Reported in the check; no rollback |
enable_automatic_rollbacks is off |
Check reports unhealthy; no rollback |
Rollback History
Every rollback (automatic or manual) is stored in the safety_rollback_records table
(feature_flag_id, safety_config_id, trigger_type, trigger_reason, previous_percentage,
target_percentage, success, executed_by_user_id, created_at). safety_config_id is the flag's
own safety configuration, or null for a flag that has none and is checked against the global defaults: a
rollback does not give such a flag a configuration, so it stays on the defaults. The rollback response includes the
record's id as rollback_record_id. No endpoint lists the records, and the dashboard lists only the
manual rollbacks made in the current browser session, so query the table for automatic ones.
Manual Rollback
Requires a superuser. Roll a flag down to a percentage from 0 to 100 (default 0; any
other value answers 422), with a reason. The
reason is a query parameter, so its spaces are written %20:
curl -s -X POST "localhost:8000/api/v1/safety/feature-flags/$FLAG_ID/rollback?percentage=0&reason=Checkout%20errors%20observed%20in%20Datadog" \
-H "Authorization: Bearer $TOKEN" \
| jq '{success, message, previous_percentage, new_percentage, deactivated: .details.deactivated}'
{
"success": true,
"message": "Feature flag 'new-checkout-flow' turned off and rolled back from 50% to 0%",
"previous_percentage": 50,
"new_percentage": 0,
"deactivated": true
}
The response also carries feature_flag_id, trigger_type (manual), rollback_record_id,
timestamp and details: the reason, deactivated, and paused_schedules (the ids of the
rollout schedules the rollback paused). A rollback that would change nothing returns
success: false, writes nothing, and says why: the flag is archived, the flag is already off,
or, for a target above 0, its global percentage is already at or below the target.
A manual rollback changes the same thing as an automatic one (see
What a rollback changes). This one was to 0%, so the flag is now off:
curl -s localhost:8000/api/v1/feature-flags/$FLAG_ID \
-H "Authorization: Bearer $TOKEN" | jq '{status, rollout_percentage}'
It prints "status": "inactive" and "rollout_percentage": 0.
Re-Enabling After Rollback
After investigating and resolving the root cause, bring the flag back in this order.
The percentage the flag had before the rollback is the rollback response's previous_percentage
(50 above). For an automatic rollback, read it from the flag's latest safety_rollback audit
entry (GET /api/v1/audit-logs/entity/feature_flag/{flag_id}, for superusers, ADMIN and
ANALYST): that entry's new_value is a JSON string holding previous_percentage. The flag's
latest row in the safety_rollback_records table holds it too.
After a rollback to 0%, turn the flag on first. It comes back at 0%, so at first only users
matched by a targeting rule are served:
curl -s -X POST localhost:8000/api/v1/feature-flags/$FLAG_ID/activate \
-H "Authorization: Bearer $TOKEN" | jq '{status, rollout_percentage}'
It prints "status": "active" and "rollout_percentage": 0.
Then raise the rollout, back to previous_percentage or, more cautiously, below it. After a
partial rollback (1–100%) the flag is still on, so this is where you start:
curl -s -X PUT localhost:8000/api/v1/feature-flags/$FLAG_ID \
-H "Authorization: Bearer $TOKEN" \
-H 'content-type: application/json' \
-d '{"rollout_percentage": 5}' | jq '{status, rollout_percentage}'
It prints "status": "active" and "rollout_percentage": 5.
Finally, resume the rollout schedule. The rollback paused the flag's active schedule, and it
stays paused until you resume it explicitly with POST /api/v1/rollout-schedules/{schedule_id}/activate
(see Rollout Schedules). The ids are in the rollback response's
details.paused_schedules.
Check the flag's thresholds. A rollback does not change the flag's safety configuration: a
flag with one of its own keeps its thresholds, and a flag without one is still checked against the
global defaults. A flag that an earlier release gave a configuration with no thresholds at a
rollback keeps that configuration, and is not checked against the defaults; if it should be
monitored, POST its thresholds (see
Create or Update a Flag's Configuration).
Watch the safety check for at least one full 15-minute window before expanding further. With automatic rollbacks on, the monitor rolls the flag back again on its next pass while one of its critical thresholds is still breached.
Best Practices
Set thresholds relative to baseline
Measure your baseline error rate and latency before enabling safety monitoring. If your API normally returns
2% errors, a 1% threshold causes false positives. A reasonable critical threshold is 2× your baseline:
baseline error rate 2% → critical_threshold: 0.04; baseline p95 latency 200 ms → critical_threshold: 400.
Use a warning threshold as an early signal
Pair a lower warning_threshold with the critical one; warnings show up in the check details and in the
dashboard without touching traffic.
Use tight thresholds for critical paths
For features that touch payments, authentication or core user flows: error_rate critical 0.02,
p95_latency critical 300.
Combine with gradual rollouts
Safety monitoring is most effective with gradual rollouts. A problem affecting 5% of users is much easier to contain than one affecting 100%. See Rollout Schedules.
Flags with large targeting rules
For a flag with large targeting rules, either set no threshold on latency, avg_latency,
p95_latency or max_latency, or leave automatic rollback off. A flag without a safety
configuration of its own uses the global default thresholds, so to take a latency threshold off one
flag, give that flag its own configuration without one.
Test the rollback path
Before a major launch, trigger a manual rollback in staging to confirm that the flag turns off as expected. A manual rollback sends no Slack alert or webhook event; only an automatic rollback does.