Zuplo logo

Canary Routing

Gradually roll out new backend versions by routing a percentage of traffic to canary before full deployment.

Prerequisite: You need a Zuplo account to run this example. Sign up for free

This example demonstrates how to implement canary deployments at the API gateway level. It routes a configurable percentage of traffic to a canary backend while the rest goes to production, with support for user allow-lists and explicit header overrides.

This pattern is useful for:

  • Gradual rollouts: Test new backend versions with a small percentage of traffic before full deployment
  • Beta programs: Route specific users to canary builds regardless of percentage settings
  • Testing in production: Use the x-stage header to manually test canary backends without affecting other traffic

Prerequisites

  • A Zuplo account. You can sign up for free.
  • A Zuplo project (required for API key authentication)

Working with this Example

Locally

Working locally is the best way to explore and understand the code for this example. You can get a local version by using the Zuplo CLI:

Terminalbash
npx create-zuplo-api@latest --example canary-routing

Then install dependencies:

Terminalbash
cd canary-routing
npm install

Before running the dev server, you need to complete the setup steps below.

Deploy this example to Zuplo

It is also possible to deploy this example directly to your Zuplo account and work with it via the Zuplo Portal. You can do this by clicking the Deploy to Zuplo button anywhere on this page.

Setup

1. Set Up Environment Variables

This example requires environment variables to configure the backend URLs and canary settings.

For local development:

Copy the example environment file:

Terminalbash
cp env.example .env

The example includes two mock APIs (powered by Mockbin) that work out of the box:

text
API_URL_PRODUCTION=https://9b720748f6564204b6e2e0baa095d779_oas.api.mockbin.io
API_URL_CANARY=https://3e99109e442f4df598c518114b250e0d_oas.api.mockbin.io
CANARY_PERCENTAGE=10
CANARY_USERS=
VariableDescriptionExample
API_URL_PRODUCTIONThe production backend URLhttps://api.example.com
API_URL_CANARYThe canary backend URLhttps://canary.api.example.com
CANARY_PERCENTAGEPercentage of traffic to route to canary (0-100)10
CANARY_USERSComma-separated list of consumer names to always route to canarybeta-tester-alice,beta-tester-bob

For deployed projects:

You must also set these environment variables in the Zuplo Portal:

  1. Go to your project in the Zuplo Portal
  2. Navigate to Settings → Environment Variables
  3. Add all four environment variables with your configuration

2. Set Up the API Key Service

This example uses API key authentication to identify users. This is required for the user allow-list feature and enables all requests to be authenticated.

  1. In the Zuplo Portal, open your project
  2. Navigate to Services → API Key Service
  3. Click Configure to create an API key bucket (if you haven't already)

To connect your local development environment to Zuplo services (API keys, rate limiting, etc.), run:

Terminalbash
npx zuplo link

Follow the prompts to select your account, project, and environment. This creates a .env.zuplo file that connects your local gateway to Zuplo's services.

Note: Don't commit .env.zuplo or .env to version control. These files contain environment-specific configuration.

For more details, see Connecting to Zuplo Services Locally.

4. Create API Keys

Create API key consumers for testing. You'll need at least one API key to make authenticated requests.

  1. In the Zuplo Portal, go to Services → API Key Service
  2. Click Create Consumer
  3. Enter a name (e.g., test-user)
  4. Click Create and copy the generated API key

Setting Up Canary Users (Optional)

To always route specific users to the canary backend, add their consumer names to the CANARY_USERS environment variable. The canary routing policy checks if request.user.sub (which contains the consumer name) matches any entry in this list.

For example, if you create consumers named beta-tester-alice@company.xyz and beta-tester-bob@company.xyz and want them to always hit canary:

text
CANARY_USERS=beta-tester-alice@company.xyz,beta-tester-bob@company.xyz

When these users make requests with their API keys, they will always be routed to the canary backend, regardless of the percentage setting.

How It Works

The canary routing policy evaluates each request using the following priority:

text
1. User Allow-List → If user ID is in CANARY_USERS, route to canary
2. Explicit Header  → If x-stage: canary header is present, route to canary
3. Percentage-Based → Hash the session/IP and route to canary if under threshold
4. Default          → Route to production

Sticky Routing

For percentage-based routing, the policy uses consistent hashing to ensure the same client always hits the same backend. This prevents users from being randomly switched between backends on each request.

The hash is calculated from:

  1. The x-session-id header (if present)
  2. The true-client-ip header (fallback)
  3. A static value "unknown" (if neither is available)

Routing Decision Flow

text
Request Arrives


┌─────────────────────┐
│ Is user in          │──Yes──▶ Route to Canary
│ CANARY_USERS list?  │
└─────────────────────┘
      │ No

┌─────────────────────┐
│ Is x-stage header   │──Yes──▶ Route to Canary
│ set to "canary"?    │
└─────────────────────┘
      │ No

┌─────────────────────┐
│ Is hash < CANARY_   │──Yes──▶ Route to Canary
│ PERCENTAGE?         │
└─────────────────────┘
      │ No

   Route to Production

Project Structure

text
├── config/
│   ├── routes.oas.json    # OpenAPI spec with route definitions
│   └── policies.json      # Policy configuration
├── modules/
│   └── canary-routing.ts  # Custom policy implementing the routing logic
├── docs/                  # Zudoku documentation portal
├── env.example            # Example environment variables
├── .env.zuplo             # Generated by `zuplo link` (do not commit)
└── zuplo.jsonc            # Zuplo project metadata

Key files to explore:

  • modules/canary-routing.ts: The custom inbound policy that determines which backend to route to based on user, header, or percentage.
  • config/routes.oas.json: Defines the API routes using OpenAPI format. Each route uses the URL Rewrite handler with ${context.custom.backendUrl} to dynamically select the backend.
  • config/policies.json: Configures the API key authentication and canary routing policies.

API Endpoints

This example exposes a simple payments API to demonstrate the routing:

MethodPathDescription
GET/v1/balanceRetrieve account balance
POST/v1/chargesCreate a new charge
GET/v1/charges/{chargeId}Retrieve a charge by ID

Running the Example

Start the API Gateway:

Terminalbash
npm run dev

The server will start on http://localhost:9000.

Testing the Routing

All requests require an API key. Replace YOUR_API_KEY with the key you created in step 4.

Testing with the x-stage Header

The easiest way to test canary routing is using the explicit header:

Terminalbash
# Route to production (default)
curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer YOUR_API_KEY"

# Route to canary via header
curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-stage: canary"

With the included mock APIs, you can verify routing by checking the livemode field in the response:

  • Production backend: Returns "livemode": true
  • Canary backend: Returns "livemode": false

Testing Percentage-Based Routing

To test percentage-based routing, set CANARY_PERCENTAGE to a value like 50 and make requests with different session IDs:

Terminalbash
# Each unique session ID will consistently route to either canary or production
curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-session-id: user-session-123"

curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-session-id: user-session-456"

Testing User Allow-List Routing

If you've added consumer names to CANARY_USERS, those users will always be routed to canary.

For example, if you created a consumer named beta-tester-alice@company.xyz and set CANARY_USERS=beta-tester-alice@company.xyz:

Terminalbash
# This user's consumer name is in CANARY_USERS → always routes to canary
curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer BETA_TESTER_ALICE_API_KEY"

# This user's consumer name is NOT in CANARY_USERS → routes based on percentage
curl http://localhost:9000/v1/balance \
  -H "Authorization: Bearer REGULAR_USER_API_KEY"

The canary routing policy checks if request.user.sub (the consumer name from the API key) matches any entry in the CANARY_USERS list.

Extending This Example

  • Multiple canary tiers: Extend the policy to support multiple canary environments (e.g., alpha, beta, canary)
  • Feature flags: Combine with feature flag metadata to enable canary routing per-feature
  • Metrics and logging: Add custom metrics to track canary vs production traffic ratios
  • Automatic rollback: Integrate with error rate monitoring to automatically disable canary routing if errors spike
  • JWT authentication: Replace API key auth with JWT to use claims for user identification

Troubleshooting

If requests aren't routing correctly or you're getting errors, work through this checklist:

  • Created a Zuplo project in the portal
  • Configured the API Key Service in Services → API Key Service
  • Ran npx zuplo link and selected your project/environment
  • Created at least one API key consumer
  • Copied env.example to .env (mock backend URLs are included)
  • Started the dev server with npm run dev

Common errors:

ErrorCauseFix
401 UnauthorizedMissing or invalid API keyCheck you're passing the Authorization: Bearer header with a valid key
Requests always go to productionCANARY_PERCENTAGE is 0 or not setSet a percentage value or use the x-stage: canary header
API_URL_CANARY not configured warning in logsCanary URL not setAdd API_URL_CANARY to your .env file
Inconsistent routingMissing session/IP identifierEnsure requests include x-session-id or come from identifiable IPs
User allow-list not workingConsumer name mismatchVerify the API key consumer's name exactly matches an entry in CANARY_USERS (e.g., if consumer is named beta-tester-alice, use CANARY_USERS=beta-tester-alice)
API Key Service errorsNot linked to ZuploRun npx zuplo link and select your project

Learn More

Related Examples

Explore more examples in this category

Environment-Based Routing

Routing

Route requests to sandbox or production backends based on API key metadata, like Stripe.

Starter

Basic API Gateway

Authentication

Protect your backend API with authentication, rate limiting, and request validation.

Starter

Dev Portal with API Keys

Authentication

Let developers sign up and create their own API keys through a self-serve portal.

Backend for Frontend (BFF) Auth

Authentication

Secure your web app by handling authentication server-side without exposing tokens to the browser.