Skip to content

Roll out with shadow mode, then enforce

Try a plan on real traffic without blocking anyone. When the “would denies” look right, switch to enforce from the console, with no deploy.

Every plan has a mode. The mode never changes the decision. It only tells your code what to do with a deny.

enforce shadow
On allow Run the tool. Run the tool.
On deny Don’t run the tool. Run the tool anyway. The deny is recorded as a “would deny”.
Use it for Real limits Trying a plan on real traffic without blocking anyone
The one rule for your code: run the tool if decision is allow or mode is shadow.
  1. New plans you create in the console start in shadow: the mode menu on Add plan says “shadow (start here)”. The two sandbox plans you get at sign-up, pilot and prod, are in enforce, so the “6th call is a deny” demo works.

  2. A deny is always HTTP 200, in both modes. Check the mode field in the answer and apply the rule:

    python
    if status != 200:
    e = body["error"] # branch on e["code"], never on the message
    ...
    if body["decision"] == "allow":
    print(f"allow: run the tool ({left}, request_id {request_id})")
    return 0
    if body["mode"] == "shadow":
    print(f"would deny (shadow): {body['reason']}, run the tool anyway ({left}, request_id {request_id})")
    return 0
    print(f"deny: {body['reason']}, don't run the tool ({left}, resets {body['resets_at']}, request_id {request_id})")
    return 2

    Leave mode out of your requests so the plan’s setting applies. If you send "mode": "shadow" or "mode": "enforce", that pins it for the call and the console switch has no effect.

  3. Allowed calls count toward the plan limit the same way in both modes and denies never do, so the count is already right when you switch. Denies are always free.

    • Overview: “N calls in the last 7 days would have been denied (shadow mode, so they ran)”, with a link to the Plans page.
    • Decisions: the decision column reads would deny (call ran), and the mode column says shadow.
    • Export (CSV or JSON): a shadow deny has decision = deny and mode = shadow. Filter on the mode column to separate the two.
    1. Open the Plans page.
    2. On the plan’s row, click switch to enforce.

    Your running server picks up the new mode on its next call, with no deploy, as long as your code doesn’t pin a mode. The switch is recorded in Console → Change log. To roll back, click switch to shadow.

Prefer our Node SDK? Install it with npm install ordainer; the Node / Express quickstart walks through it.

With mode unset (the default), the SDK follows the plan’s mode from each answer. Your server log shows a line like this for a shadow deny:

log
[ordainer] shadow: would deny tool=research.run reason=quota_exceeded remaining=0/5 request_id=req_… (call allowed)

To pin a mode instead:

const ordainer = new Ordainer({ mode: 'shadow' });

A pinned mode overrides the console. Remove the pin to follow the console again. To send would-denies to your own metrics, pass onDecision: (d) => …; d.wouldDeny is true for shadow denies.

Your caller never sees a would-deny: in shadow the tool runs and returns its normal answer. Before you copy anything from req.ordainer into a response, read What your caller sees in shadow.

Shadow mode never blocks a call because of a plan or test limit. After your test keys reach their monthly limit, each shadow call still counts and is logged as a would-deny with the reason test_limit_reached, and the answer still tells your tool to run. To protect the service, shadow calls have a safety ceiling of 10 times your test limit. Past it, they return HTTP 429 with safety_limit_reached until your test usage resets at the start of the next month (UTC), and those calls aren’t counted or recorded. That’s a safety limit, not a deny. Our SDK (version 1.2.0 and later) lets the tool run automatically. If you call our API directly, we recommend letting the tool run and logging the error. If you reach it, email support@ordainer.ai.

Shadow is about what to do with a real decision. Fail open / fail closed is about what to do when we can’t answer (timeout, network error, server error). They are separate settings. See Fail open vs fail closed.