FeatureSignals

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

You'll create a kill switch for a third-party payment integration. When the payment provider has an outage, you can flip the kill switch to OFF — instantly falling back to a cached or queued payment flow without impacting your users.

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

While all feature flags can serve as kill switches, a dedicated kill switch flag has different characteristics: it should default to ON(not OFF), be categorized as 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.

Create kill switch via APIBash
1
2
3
4
5
6
7
8
9
10
11
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.

Node.js — Payment service with kill switchTypeScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
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;
  }
}
Go — HTTP middleware kill switchGo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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)
Python — Decorator-based kill switchPython
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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:

  1. Toggle response time: Flip the kill switch OFF in staging and measure how long until your app starts returning the fallback behavior.
  2. Fallback correctness: Verify the fallback path actually works — queued payments are persisted, users see the right message, no data is lost.
  3. Re-enable response time: Flip the kill switch back ON and verify the primary path resumes correctly.
  4. Monitoring alert: Verify your alert fires when the kill switch is toggled.
Fire drill automation scriptBash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/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 ==="

Next Steps