Kill Switch Pattern
A kill switch is a feature flag designed to be flipped OFF in an emergency — instantly disabling a feature, integration, or code path without waiting for a deployment. It's your fastest rollback mechanism.
What you'll build
When to Use a Kill Switch
Kill switches are ideal for high-risk or externally-dependent functionality:
- Third-party API integrations (payment, email, SMS, mapping)
- New infrastructure that may degrade under load
- Experimental features with unknown failure modes
- Features with regulatory or compliance risk
- Code paths that touch customer billing or PII
Kill switches vs. feature flags
ops (indefinite lifespan), and be tested regularly with fire drills.Step 1: Create the Kill Switch Flag
Create an ops-category flag with a default value of true. This means "feature is active" by default, and flipping it OFF disables the feature.
curl -X POST https://api.featuresignals.com/v1/projects/{projectID}/flags \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"key": "killswitch-payment-provider",
"name": "Payment Provider Kill Switch",
"type": "boolean",
"defaultValue": true,
"toggleCategory": "ops",
"description": "Emergency kill switch for the third-party payment provider. Flip OFF to disable and fall back to queued/cached payments."
}'
The opscategory tells FeatureSignals this flag should never be marked stale — it's expected to live indefinitely.
Step 2: Wire the Kill Switch Into Your App
Wrap the risky code path with the kill switch. The flag is checked on every request to ensure the fastest possible reaction to an emergency toggle.
import { FeatureSignalsClient } from '@featuresignals/node';
const client = new FeatureSignalsClient(process.env.FS_API_KEY!, {
envKey: 'production',
});
await client.waitForReady();
interface PaymentRequest {
userId: string;
amount: number;
currency: string;
paymentMethodId: string;
}
async function processPayment(req: PaymentRequest) {
// Check kill switch FIRST — before any payment processing
const providerActive = client.boolVariation(
'killswitch-payment-provider',
{ key: req.userId },
true, // Default ON — if SDK is unreachable, keep the feature active
);
if (!providerActive) {
// Kill switch is OFF — fall back to queued payment
logger.warn('Payment provider kill switch active — queuing payment', {
userId: req.userId,
amount: req.amount,
});
return queuePaymentForLater(req);
}
// Normal path — call the third-party provider
try {
const result = await paymentProvider.charge(req);
return result;
} catch (error) {
logger.error('Payment provider failed', { error, userId: req.userId });
// Optionally check kill switch again — provider might have been disabled
// during the request due to cascading failures
const stillActive = client.boolVariation(
'killswitch-payment-provider',
{ key: req.userId },
true,
);
if (!stillActive) {
return queuePaymentForLater(req);
}
throw error;
}
}
package middleware
import (
"net/http"
fs "github.com/featuresignals/sdk-go"
)
// KillSwitchMiddleware wraps an HTTP handler with a kill switch check.
// If the kill switch flag is OFF, the handler returns a 503 with a
// Retry-After header instead of processing the request.
func KillSwitchMiddleware(
client *fs.Client,
flagKey string,
) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
// Check kill switch — default ON
active := client.BoolVariation(
flagKey,
fs.NewContext(userID),
true,
)
if !active {
w.Header().Set("Retry-After", "60")
http.Error(w, "Service temporarily unavailable", http.StatusServiceUnavailable)
return
}
next.ServeHTTP(w, r)
})
}
}
// Usage in router setup:
// r := chi.NewRouter()
// r.Use(KillSwitchMiddleware(fsClient, "killswitch-payment-provider"))
// r.Post("/api/payments", paymentHandler)
from functools import wraps
from featuresignals import FeatureSignalsClient, EvalContext
client: FeatureSignalsClient = None # Initialized at app startup
def kill_switch(flag_key: str):
"""Decorator that gates a function behind a kill switch flag."""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Extract user context — adjust based on your framework
user_id = kwargs.get("user_id", "unknown")
active = client.bool_variation(
flag_key,
EvalContext(key=user_id),
True, # Default ON
)
if not active:
raise ServiceUnavailableError(
f"Kill switch '{flag_key}' is active"
)
return func(*args, **kwargs)
return wrapper
return decorator
# Usage
@kill_switch("killswitch-payment-provider")
def process_payment(user_id: str, amount: int, currency: str):
return payment_provider.charge(user_id, amount, currency)
Step 3: Enable in All Environments
Kill switches should be enabled (ON) in all environments from day one. They exist as a safety net — always ready, rarely used. Configure them:
- Production: Enabled, 100% rollout (the kill switch is ON; the feature is active)
- Staging: Enabled, 100% rollout (test the kill switch in staging regularly)
- Development: Enabled, 100% rollout (developers can toggle locally to test fallback paths)
Step 4: Create a Runbook
Every kill switch needs a runbook so any on-call engineer can operate it. Document the following:
Kill Switch Runbook: Payment Provider
- Flag Key
- killswitch-payment-provider
- Dashboard Link
- https://app.featuresignals.com/flags/killswitch-payment-provider
- Default State
- ON (feature active, kill switch dormant)
- When to Flip OFF
- Payment provider outage, elevated error rates > 5%, provider security incident, or on-call manager directive
- Effect of OFF
- Payments are queued; users see 'Payment processing — confirmation email will be sent within 15 minutes'
- Fallback Behavior
- Payment queued to outbox table; processed when provider recovers and kill switch is re-enabled
- Who Can Toggle
- On-call engineer with Owner or Admin role
- Notification
- Flip triggers Slack #incidents and PagerDuty alert
Step 5: Test with a Fire Drill
Kill switches that aren't tested don't work. Schedule regular fire drills to verify:
- Toggle response time: Flip the kill switch OFF in staging and measure how long until your app starts returning the fallback behavior.
- Fallback correctness: Verify the fallback path actually works — queued payments are persisted, users see the right message, no data is lost.
- Re-enable response time: Flip the kill switch back ON and verify the primary path resumes correctly.
- Monitoring alert: Verify your alert fires when the kill switch is toggled.
#!/bin/bash
# killswitch-drill.sh — automated fire drill for a kill switch
FLAG_KEY="killswitch-payment-provider"
API_KEY="$FS_API_KEY"
BASE_URL="https://api.featuresignals.com"
echo "=== Kill Switch Fire Drill: $FLAG_KEY ==="
echo ""
# 1. Record start time
START=$(date +%s)
# 2. Flip kill switch OFF
echo "[1/4] Flipping kill switch OFF..."
curl -s -X PATCH \
"$BASE_URL/v1/flags/by-key/$FLAG_KEY/environments/production" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": false}' > /dev/null
# 3. Wait for propagation and test
echo "[2/4] Waiting for propagation (30s)..."
sleep 30
# 4. Verify fallback
echo "[3/4] Verifying fallback behavior..."
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "https://api.example.com/payments" \
-H "X-User-ID: drill-test-user" \
-d '{"amount": 100, "currency": "USD"}')
if [ "$RESPONSE" = "202" ]; then
echo " ✓ Fallback working (HTTP $RESPONSE — payment queued)"
else
echo " ✗ Unexpected response: HTTP $RESPONSE"
fi
# 5. Re-enable kill switch
echo "[4/4] Re-enabling kill switch..."
curl -s -X PATCH \
"$BASE_URL/v1/flags/by-key/$FLAG_KEY/environments/production" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": true}' > /dev/null
END=$(date +%s)
DURATION=$((END - START))
echo ""
echo "=== Drill complete in ${DURATION}s ==="