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.
What your caller sees in shadow
Section titled “What your caller sees in 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.
Where the mode comes from
Section titled “Where the mode comes from”- The
modefield 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 samemode(or none, if the first call sent none); otherwise it gets409 idempotency_conflict. - 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.
What stays the same in both modes
Section titled “What stays the same in both modes”- 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 keepdecision: denywithmode: 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.
At the test limit: test_limit_reached
Section titled “At the test limit: test_limit_reached”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.
Defaults
Section titled “Defaults”| 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 |
In the Node SDK
Section titled “In the Node SDK”modeunset (the default): follow the plan’s mode from each answer.mode: 'shadow'or'enforce'on the constructor,middleware(),wrap()ordecide(): pinned, and sent asmodein the request. A pinned mode overrides the console.result.allowedalready applies the rule above.result.wouldDenyistrueonly for a shadow deny (the call ran); an enforce deny hasdecision: 'deny',allowed: falseandwouldDeny: false(SDK 1.2.2; 1.2.1 and earlier setwouldDenyon every deny).
Not the same thing as fail open
Section titled “Not the same thing as fail open”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.
Related
Section titled “Related”- How to roll out with shadow and then switch to enforce: the help article “Shadow mode vs enforce: test safely, then turn blocking on”.
- Decide → What
modemeans for your code