Skip to content

Shadow vs enforce

Every plan has a mode: shadow or enforce. 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”.
Who acts on it Your code (or the Node SDK) Your code (or the Node SDK)
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.

Nothing different, and that’s on purpose. In shadow we still make the real decision and record it, but your tool runs, so the caller (an agent, or the person behind it) gets your tool’s normal answer, usually HTTP 200, even when we would have said no. The Node SDK doesn’t add anything to that answer.

The would-deny is for you, not your caller. You’ll find it in your server log ([ordainer] shadow: would deny …), on req.ordainer.wouldDeny (or result.wouldDeny), and in Console → Decisions as “would deny (call ran)”. If you copy req.ordainer.decision into your response, as our quickstart sample does so you can watch, an agent sees a successful result with "deny" tucked inside, and it will usually treat the call as a success. Leave it out of real handlers.

When the agent should hear “no”, use enforce. Then a deny stops the call, and the caller gets a 429 with Retry-After and a sentence it can act on. Switching is one click on the plan in Console → Plans, with no deploy, unless your code pins the mode.

Shadow only changes what happens on a deny. A problem with your key, or going over a key’s traffic limits, still stops the call in either mode, because those are errors, not decisions. Would-denies don’t count toward your monthly test decisions (1,000 by default), since only allowed test decisions do. Denies are always free, on test keys and live keys.

  1. The mode field in your request, if you send one ("enforce" or "shadow"). This pins it for that call. A retry with the same idempotency key must send the same mode (or none, if the first call sent none); otherwise it gets 409 idempotency_conflict.
  2. Otherwise, the plan’s mode, set in Console → Plans. Changing it there takes effect on the next call, with no deploy, and is recorded in Console → Change log.

The answer’s mode field always tells you which one applied.

  • The decision. The same counter, the same cap, the same reason.
  • Counting. Allows are counted in both modes. Denies never count toward a key’s plan limit or your monthly test decisions, in either mode. So when you switch a plan from shadow to enforce, the counter is already right.
  • The audit log. Every allow and deny writes one row with its mode. The console’s Decisions page shows a shadow deny as “would deny (call ran)”; exports keep decision: deny with mode: shadow.
  • The monthly decision ceiling on your subscription counts allowed live decisions in both modes. Denies, including shadow “would denies”, never count.
  • The test-key limit. Only allowed test-key decisions count toward your organization’s monthly test-key limit, shadow or not. Shadow would-denies are free, but they do count toward the safety ceiling.

When your organization has used its free test-key decisions for the month, test keys get deny / test_limit_reached, with the call’s own mode like any other deny. In enforce it’s a real deny: don’t run the tool (the call isn’t counted). In shadow it’s a “would deny” and the tool runs; see Shadow mode at the test limit for the safety ceiling. Live keys are never affected.

Plan Mode it starts in
The two sandbox plans you get at sign-up (pilot, prod) enforce, so the “6th call is a deny” demo works
Plans you create in the console shadow
  • mode unset (the default): follow the plan’s mode from each answer.
  • mode: 'shadow' or 'enforce' on the constructor, middleware(), wrap() or decide(): pinned, and sent as mode in the request. A pinned mode overrides the console.
  • result.allowed already applies the rule above. result.wouldDeny is true only for a shadow deny (the call ran); an enforce deny has decision: 'deny', allowed: false and wouldDeny: false (SDK 1.2.2; 1.2.1 and earlier set wouldDeny on every deny).

Shadow is about what to do with a real decision. Fail open / fail closed is about what to do when Ordainer can’t answer (timeout, network error, server error). They are separate settings. An error answer (any 4xx) is neither: the Node SDK blocks it in every mode and every failMode, shadow included. There are two exceptions: your own lapsed subscription (403 subscription_inactive), which follows the separate billing-lapse setting (billingLapse, default open), and 429 safety_limit_reached (shadow calls past the safety ceiling), which SDK 1.2.0 runs in every failMode. See Fail open vs fail closed.