Skip to content

Product Engineering

Canary Deploys + Gradual Rollouts — Shipping Safely on Netlify

Netlify deploys your code to 100% of edge nodes in seconds. That's the good news. The bad news: if there's a bug, 100% of users see it instantly. Canary deploys flip that: your code ships everywhere, but feature flags control who runs it. Ship new logic to 5% of users first. Monitor for 2 hours. If error rates stay normal and conversion holds, roll to 25%. Then 100%. At any step, if something breaks, flip the flag off — no redeploy, no downtime. This post covers the definition, why canary beats big-bang deploys, the flag + middleware pattern, production rollout strategies, Sentry monitoring, rollback tactics, 6 FAQs, and how to actually ship this way on Netlify.

🚀 🐤 📊

It's Tuesday morning. You ship a new checkout flow. You've tested it. You're confident. Netlify builds. The code is live globally in 90 seconds. Within minutes, your Sentry dashboard lights up: checkout completion drops 12%. You're losing $5k/hour. You panic. You revert. You redeploy. 20 minutes of full downtime. By the time the old code is back, you've lost $1,600 in revenue and your customers are tweeting about your outage.

Or: you ship the same code. Netlify builds. It's live globally. But your feature flag sets rollout to 5% — only 1 in 20 checkouts use the new flow. Within 30 minutes, Sentry shows elevated errors. You flip the flag to 0%. Rollout stops instantly. No redeploy. No downtime. You spend an hour debugging. You fix the bug. You redeploy (the code was already live, so Netlify just flips a flag). You test with 1% again. This time, metrics look good. You roll to 100% over the next 2 hours. Total user impact: maybe 50 checkouts saw the bug. Your customers don't notice.

That's the canary deploy. Ship code everywhere. Control activation with flags. Catch bugs on 5% instead of 100%.

What Is a Canary Deploy?

A canary deploy is a risk-reduction pattern: you ship new code to all your infrastructure, but only activate it for a small percentage of users. The name comes from coal miners, who brought canaries into mines to detect toxic gas — if the canary died, the miners knew something was wrong and evacuated. In modern deploys, your early users are the canary.

The pattern: (1) ship code, (2) feature flag gates the new behavior at 0% (off), (3) turn on the flag for 1% of users, (4) monitor error rates and key metrics for 30 minutes to an hour, (5) if all looks good, gradually increase to 5%, then 25%, then 50%, then 100% over the course of 24–48 hours. At any step, if something breaks, flip the flag to 0% and the issue stops instantly. No rollback. No redeploy. No downtime.

The key difference from big-bang deploys: you're not hoping nothing goes wrong. You're betting that something *might* go wrong, and you're shipping in a way that catches it fast and stops the bleeding before it spreads to your entire user base.

Why Canary Beats Big-Bang

Big-bang deploys (the old way): You merge to main, CI passes, you deploy. The code is live for 100% of users instantly. If there's a bug, every user hits it at once. Your error rate spikes. Your conversion rate drops. By the time you notice, thousands of users have already been affected. You either live with the bug for 30 minutes while you debug, or you rollback the entire deploy — which means reverting unrelated changes that happened in the same window.

Canary deploys (the Aidxn way): You merge to main, CI passes, you deploy. The code is live for 0% of users initially. You enable it for 1%, then monitor. If there's a bug, maybe 100 users hit it before you catch it. You flip the flag off. The bug is gone instantly, and the 100 users move back to the old code path on their next request. You debug at your own pace, ship a fix, and redeploy without an incident.

Canary reduces user impact from 100% to 1% (or less). It also removes the psychological pressure to rollback immediately — you can take 30 minutes to understand what broke, fix it properly, and redeploy. Big-bang deploys create the opposite incentive: panic, rollback, debug later.

The Flag + Middleware Pattern

Canary deploys live at the boundary between your code and the request. Here's the pattern you ship on Netlify Edge or in your server middleware.

Step 1: Feature Flags Table (Supabase)

CREATE TABLE features (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT UNIQUE NOT NULL,        -- 'checkout_v2', 'dashboard_redesign', etc.
  enabled BOOLEAN DEFAULT false,     -- global on/off kill switch
  rollout_percentage INTEGER DEFAULT 0,  -- 0-100, gradual rollout %
  target_users TEXT[] DEFAULT '{}',  -- internal staff / beta users
  created_at TIMESTAMP DEFAULT now(),
  updated_at TIMESTAMP DEFAULT now(),
  updated_by TEXT                    -- who made the change, for audit
);

CREATE INDEX idx_features_name ON features(name);

Step 2: Middleware that Evaluates Flags (Next.js / Astro)

For Next.js, this lives in middleware.ts. For Astro, it lives in a server endpoint or edge function. Either way: before rendering the page, ask "does this user get the new code?"

// middleware.ts (Next.js / Netlify)
import { supabase } from '@/lib/supabase';
import { createHash } from 'crypto';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';

let featureCache: Map = new Map();
let cacheExpiry = 0;

interface FeatureRule {
  name: string;
  enabled: boolean;
  rollout_percentage: number;
  target_users: string[];
}

async function getFeatures(): Promise> {
  const now = Date.now();
  if (featureCache.size > 0 && now < cacheExpiry) {
    return featureCache;
  }

  const { data } = await supabase
    .from('features')
    .select('*');

  featureCache = new Map();
  if (data) {
    data.forEach((f) => {
      featureCache.set(f.name, {
        name: f.name,
        enabled: f.enabled,
        rollout_percentage: f.rollout_percentage,
        target_users: f.target_users || [],
      });
    });
  }

  cacheExpiry = now + 60000; // Cache 60 seconds
  return featureCache;
}

function shouldEnableFeature(
  featureName: string,
  userId: string,
  feature: FeatureRule
): boolean {
  if (!feature.enabled) return false;

  // Internal staff always get new features
  if (feature.target_users.includes(userId)) {
    return true;
  }

  // Consistent hash determines rollout bucket
  const hash = createHash('sha256')
    .update(`${featureName}:${userId}`)
    .digest('hex');
  const hashInt = parseInt(hash.substring(0, 8), 16);
  const rolloutBucket = (hashInt % 100) + 1;

  return rolloutBucket <= feature.rollout_percentage;
}

export async function middleware(request: NextRequest) {
  const features = await getFeatures();
  const userId = request.headers.get('x-user-id') || 'anonymous';

  // Set feature flags as response headers for client-side code
  const response = NextResponse.next();

  for (const [featureName, feature] of features) {
    const enabled = shouldEnableFeature(featureName, userId, feature);
    response.headers.set(`x-feature-${featureName}`, enabled ? 'true' : 'false');
  }

  return response;
}

export const config = {
  matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};

Step 3: Client Hook to Read Flags

// hooks/useFeature.ts
import { useEffect, useState } from 'react';

export function useFeature(featureName: string): boolean {
  const [enabled, setEnabled] = useState(false);

  useEffect(() => {
    // Read the flag from response headers (set by middleware)
    const flagValue = document.documentElement.getAttribute(
      `data-feature-${featureName}`
    );
    setEnabled(flagValue === 'true');
  }, [featureName]);

  return enabled;
}

// pages/checkout.tsx
import { useFeature } from '@/hooks/useFeature';

export default function CheckoutPage() {
  const checkoutV2 = useFeature('checkout_v2');

  return (
    
{checkoutV2 ? : }
); }

Production Rollout Strategy

Hour 0 (deploy): Ship the code. Set rollout to 0%. Feature is completely off.

Hour 1 (canary): Set rollout to 1% (roughly 1 in 100 users). Monitor Sentry for new errors, latency for slowdowns, and conversion rates for drops. Watch your metrics dashboard.

Hour 2 (if all good): Set rollout to 5%. Watch for 30 more minutes.

Hour 3 (assuming green lights): Set rollout to 25%. This is still a small segment, but large enough to catch subtle bugs (edge cases with low-frequency event patterns).

Hour 4–12 (monitoring window): If 25% is stable, set to 50% and let it run for several hours (overnight if you're shipping in the morning).

Hour 24 (if no incidents): Set to 100%. Feature is fully live.

Total time: 24–48 hours from code ship to full production. Compare to big-bang deploys, where 100% happens in 90 seconds and you're praying nothing broke.

Canary Monitoring with Sentry

Sentry is your canary detector. Set up an alert that triggers on new error patterns during rollout.

Alert rule (Sentry): "If error rate increases by 50% compared to the previous 1 hour, ping Slack with the top 5 errors."

// In Sentry, set up a Release alert:
// Trigger: Release is deployed
// Condition: Error rate (%) > baseline + 50%
// Action: Notify Slack #incidents

// Then, tag your feature flag rollouts:
import * as Sentry from '@sentry/nextjs';

// In your rollout script:
Sentry.captureMessage('Feature rolled out: checkout_v2 → 5%', 'info', {
  tags: {
    feature: 'checkout_v2',
    rollout_percentage: 5,
    timestamp: new Date().toISOString(),
  },
});

When you bump rollout from 1% to 5%, Sentry logs it. Then if errors spike, Sentry can cross-reference the timing and tell you which rollout step caused the problem.

Instant Rollback (No Deploy Needed)

The whole point of canary deploys: if something breaks, rollback is a single SQL UPDATE.

-- Something broke at 5% rollout. Instant rollback:
UPDATE features
SET rollout_percentage = 0
WHERE name = 'checkout_v2';

-- The flag is now off globally.
-- All users (including the 5% who were seeing the new code) get the old behavior on their next request.
-- No CI pipeline. No revert commit. No 20 minute downtime.
-- Rollback time: < 1 second.

Compare to big-bang: you ship, something breaks, you revert the commit, you redeploy, you wait 90 seconds for the new code to hit all edge nodes. Total downtime: 5–10 minutes. Canary: instant.

Six FAQs

Doesn't gradual rollout mean I'm shipping slow?

You ship the code in 90 seconds (Netlify's standard deploy). The gradual rollout is about *activating* the code, not shipping it. The code lives on all your edge nodes from minute 1. You're just controlling who sees it. This is actually faster than safe big-bang deploys, which need hours of QA and testing before merge because there's no safety net.

What if my canary period is 48 hours but I want to deploy again?

You can ship new code anytime. Your current rollout (say, 25% for the old feature) persists independently. Ship the new feature behind a different flag name. Now you have two independent rollouts in flight. This is actually the power of feature flags — your deploy pipeline and your rollout schedule are decoupled.

How do I test the canary period locally?

Hardcode rollout_percentage = 5 in your local .env.local, then run the app. Or use Supabase's local dev (supabase start) and manually set the percentage in the local DB. Test as if you're a user in the 5% cohort, then one outside it.

What if a power user is stuck in the old code path?

Your consistent hash ensures they always get the same variant (for a given feature). Once rollout hits 100%, the hash doesn't matter anymore — everyone gets the new code. If they clear their cookies, the hash is recomputed based on user ID and feature name, so they get the same result. No randomness. No surprises.

Should I use canary for every deploy?

Not every change needs 48-hour rollout. Typo fixes, one-line CSS changes, and internal-only admin features can go 100% immediately. Use canary for anything that affects revenue, user-facing flows, or complex logic. The cost of a mistake (lost revenue, support load, reputational damage) should justify the overhead.

Can I use canary deploys with Netlify Analytics?

Yes. Netlify Analytics shows edge traffic. Feature flags don't live at the edge level — they live in your application — so you'll see total traffic, not per-variant traffic. For detailed per-variant analytics, log custom events to Sentry, Mixpanel, or your data warehouse during the request (before you return the variant to the user).

The Bottom Line

Big-bang deploys give you speed at the cost of risk. Canary deploys give you both: ship code in 90 seconds, but activate it slowly. Catch bugs on 5% of traffic instead of 100%. Zero-downtime rollbacks. Confident shipping at scale.

The pattern: feature flags table (5 mins to set up), middleware that evaluates flags (1 hour to write), gradual rollout schedule (you decide), Sentry monitoring (5 mins to configure), and a single SQL UPDATE for instant rollback. That's the whole toolkit.

Start with your highest-risk feature — checkout, billing, auth, or something that breaks revenue if it breaks. Ship it behind a flag. Roll out 1% → 5% → 25% → 100% over 48 hours. Watch the metrics. Once you've done it once, every deploy after that will feel riskless.

Ready to design a deployment pipeline that scales with your product? Check out Aidxn Design's product engineering consulting, or read our full guide on feature flags and rollout strategies to pair canary deploys with A/B testing.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.