Vortos
Feature Flags

Release Guardrails

Policies that block risky flag operations — prevent disabling a flag that would break an SLO, or rolling back a flag while a dependent service is unhealthy.

Release Guardrails

A guardrail is a policy attached to a flag that can block specific write operations from executing. Guardrails run before FlagWriteService commits a change. If any guardrail policy fails, the write is rejected with an explanation.

Use guardrails when:

  • Disabling a flag would break a downstream SLO (e.g., disabling a flag that activates a circuit breaker means traffic floods an already-degraded service)
  • You want to prevent a rollback during a running A/B experiment to avoid contaminating the sample
  • A compliance rule requires a flag to remain enabled for a minimum period before it can be disabled

How guardrails work

FlagWriteService receives a request to disable 'new-payments-processor'

GuardrailPolicyService checks all active policies for this flag

Policy: "payments SLO must be above 99.5% to allow disable"
  → checks current SLO metric
  → SLO is 99.1% (degraded) → BLOCK

Write rejected — actor receives reason: "SLO below threshold (99.1% < 99.5%)"

Audit entry: write_blocked, reason, policy name

The guardrail does not make the write invisible — it is recorded in the audit log with the blocking reason so there is a trail.

Defining a guardrail policy

Implement GuardrailPolicyInterface:

use Vortos\FeatureFlags\Guardrail\GuardrailPolicyInterface;
use Vortos\FeatureFlags\Guardrail\GuardrailContext;
use Vortos\FeatureFlags\Guardrail\GuardrailResult;

final class SloGuardrailPolicy implements GuardrailPolicyInterface
{
    public function __construct(
        private readonly MetricsService $metrics,
    ) {}

    public function check(GuardrailContext $context): GuardrailResult
    {
        // Only block 'disable' operations
        if ($context->operation !== 'disable') {
            return GuardrailResult::pass();
        }

        $slo = $this->metrics->currentSlo($context->flagName);

        if ($slo < 99.5) {
            return GuardrailResult::block(
                reason: sprintf(
                    'SLO is %.1f%% — below the 99.5%% threshold required to disable this flag.',
                    $slo,
                ),
            );
        }

        return GuardrailResult::pass();
    }
}

Attach the policy to a flag via configuration or the admin UI:

php vortos vortos:flags:guardrail:attach new-payments-processor \
  --policy=SloGuardrailPolicy \
  --env=production

Built-in policies

PolicyWhat it blocks
MinimumAgePolicyBlocks disable/rollback until the flag has been enabled for N days
ExperimentActivePolicyBlocks all changes while a linked experiment is collecting data
PercentageFloorPolicyBlocks reducing rollout below a configured minimum
RequiresChangeRequestPolicyForces all writes through the change request workflow

Admin UI guardrail panel

The flag detail page shows active guardrails in a dedicated panel. Each guardrail shows:

  • Policy name and description
  • Current status (passing / blocking)
  • The blocking reason if active

This gives operators visibility into why a change might be rejected before they try to make it.

Override for emergencies

Users with the flags.manage permission can force-bypass a guardrail with a mandatory reason:

php vortos vortos:flags:disable new-payments-processor \
  --env=production \
  --bypass-guardrails \
  --reason="INC-0099: forced disable during P0 incident"

The bypass is audited. Use it only in genuine emergencies.

On this page