Skip to main content

Canary Routing

Route a small percentage of traffic to a new backend version while keeping the remainder on the stable path. When metrics confirm the canary is healthy, promote it to 100% with a single workflow publish — no service redeploy.

Typical scenario: Your team has deployed orders-service-v2 alongside the existing orders-service-v1. You want to send 10% of order API traffic to v2 to validate behavior before full rollout.


Prerequisites

  • You are signed in to the Management UI as an admin or editor
  • Two upstream services are reachable from the gateway:
    • Stable: https://orders-v1.internal
    • Canary: https://orders-v2.internal
  • A collection (e.g. orders-api) and a proxy (e.g. GET /orders/{orderId}) exist and are published

How canary routing works in Zerq

The workflow uses a condition_node to evaluate a synthetic percentage condition. For each request, the condition evaluates whether a random value falls within the canary window (e.g. 0–10). If true, the request goes to the canary backend; otherwise it goes to the stable backend.

Both branches converge at a single response_node, so the client always receives one unified response.


Workflow overview


Step 1 — Open the workflow canvas

  1. Open Collections → orders-api.
  2. Select the GET /orders/{orderId} proxy.
  3. Click the Workflow tab.

Step 2 — Add the condition node

  1. Drag a Condition node from the palette and connect it from http_trigger.
  2. Name it canary-split.
  3. Set the condition expression:
{{math.random(100)}} < 10

This evaluates true for approximately 10% of requests and false for 90%.

tip

To adjust the canary percentage, change 10 to any value from 1 to 100. To promote the canary to 100%, change the condition to always evaluate true, or replace both branches with a single stable backend call.


Step 3 — Add the canary backend node (true branch)

  1. Connect an http_request_node from the true output of canary-split.
  2. Configure:
    • Name: call-v2-canary
    • Method: GET
    • URL: https://orders-v2.internal/orders/{{trigger.params.orderId}}
    • Headers: forward any required auth headers from {{trigger.headers}}

Step 4 — Add the stable backend node (false branch)

  1. Connect an http_request_node from the false output of canary-split.
  2. Configure:
    • Name: call-v1-stable
    • Method: GET
    • URL: https://orders-v1.internal/orders/{{trigger.params.orderId}}
    • Headers: forward the same auth headers

Step 5 — Converge at the response node

  1. Drag a Response node.
  2. Connect both call-v2-canary and call-v1-stable to this node.
  3. Configure the response to return the body from whichever branch ran:
Body: {{lastSuccessfulNode.response.body}}
Status: {{lastSuccessfulNode.response.status}}
note

lastSuccessfulNode resolves to whichever upstream node completed for this request path.


Step 6 — Save and publish

  1. Click Save on the canvas.
  2. Click Publish on the proxy.

Traffic will immediately begin splitting: approximately 10% to v2, 90% to v1.


Verify the split

Send 20 requests and observe distribution in logs:

for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" \
https://gateway.example.com/orders-api/orders/order-99 \
-H "X-Client-ID: <client-id>" \
-H "Authorization: Bearer <token>" \
-H "X-Profile-ID: <profile-id>"
done

Open Logs → Request Logs and filter by the orders-api collection. Check the upstream URL column — you should see roughly 2 out of 20 requests hitting orders-v2.internal and 18 hitting orders-v1.internal.


Promoting or rolling back

To promote canary to 100%:

  1. Open the canary-split condition node.
  2. Change the expression to true (always true), or remove the condition and route directly to call-v2-canary.
  3. Save and publish.

To roll back immediately:

  1. Open the canary-split condition node.
  2. Change the expression to false (always false), routing all traffic to call-v1-stable.
  3. Save and publish.

Both changes take effect immediately after publish with no gateway restart.


Advanced: route by header instead of percentage

For more deterministic testing (e.g. QA team always hits v2), use a header-based condition instead:

{{trigger.headers["X-Canary"]}} == "true"

Requests with X-Canary: true go to v2; all other requests go to v1.


Next steps