MainSDK
Search⌘K
Interfaces

Observability and analytics

Behaviour norms

norms

Learn what normal looks like for the app (traffic, errors, latency, CPU, loop delay, memory) by hour of week, deterministically and with no model, and emit an anomaly signal when it leaves the range.

Building for 0.4ships as a beta APIPhase 1 · Core, public betaSpec normsFree in your app, Apache-2.0

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

norms lets your app learn what normal looks like for itself, by hour of week. When a metric leaves that range and stays out, it emits one anomaly.detected signal.

At a glance

norms:
  season: hour-of-week
  tz: Asia/Kolkata
  learn: 7d
  min-samples: 20                      # observations a bucket needs before it speaks
  persist: 5m                          # how often the model is stored, 1s to 24h

Calls

  • norms.get(metric, { at? })
  • norms.list({ cursor?, limit? })
  • norms.delete(metric, { reason })
  • new Norms(opts)
  • parseNorms(section?)
  • normsIssues(section)

Guarantees

Whatever the driver.

  • The same input gives the same model and the same signals
  • No dependency, no model and no service
  • Holds numbers only, never a person

Drivers

1 drivers. Select one to see its line in main.yaml.

main.yamlBuilt in (default)
norms:
  driver: builtin

Change this one line to swap providers; your calls stay the same.Built-in and local drivers integration

Codes

CodeWhere it appearsMeaning
MAIN-NORMS-0001Checkmainsdk check, before deployA norms value is invalid: every outside 1m to 15m, an unknown season, metric or time zone, learn over 28d, min-samples under 10, z under 2, or margin outside 0% to 200%
MAIN-NORMS-0002RuntimeError responses and logsSee the spec
MAIN-NORMS-0003RuntimeError responses and logsSee the spec
MAIN-NORMS-0004RuntimeError responses and logsSee the spec

Features

Nothing here has shipped yet. Each feature lands in its phase; the spec ticket builds it.

FeaturePhaseMilestoneSpec ticket
EWMA and decayed histogram modelPhase 10.4norms#model
Seasonal bucketsPhase 10.4norms#season
Auth failures, access denials and refused egress as inputsPhase 10.4norms#inputs-security
Anomaly signalsPhase 10.4norms#anomaly
Model persisted in the runtime storePhase 10.4norms#persist

Quick start

main.yaml
norms:
  season: hour-of-week
  tz: Asia/Kolkata
  learn: 7d
app/example.ts
import { norms } from "mainsdk";

const n = await norms.get("requests");
console.log(n.bucket, n.p50, n.p99, n.learning);   // "mon-10" 118 240 true

Adding a norms section turns learning on. Without it nothing runs and norms.get throws MAIN-CONFIG-0003.

Configuration

main.yaml
norms:
  every: 1m                  # one value per metric per window; 1m to 15m
  season: hour-of-week       # hour-of-week (168 buckets) | hour-of-day (24) | none (1)
  tz: Asia/Kolkata           # time zone for buckets; default UTC
  learn: 7d                  # anomalies are info and learning: true until the model is this old; 0 to 28d
  min-samples: 30            # values a bucket needs before it is used; else hour-of-day, else global
  half-life: 14d             # how fast old behaviour fades, counted in visits to a bucket
  metrics: [cpu, loop, memory, requests, errors, latency]
  sensitivity: { z: 4, quantile: p99, margin: 20%, hold: 3 }   # hold: consecutive windows out of range
  persist: 5m
KeyWhat it does
everyThe window. Windows are aligned to the clock, so instances agree on buckets.
seasonHow values are bucketed. Scoring uses the finest bucket with min-samples values.
learnWhile the model is younger than this, anomalies are info with learning: true.
half-lifeOld behaviour fades by half after this many visits to a bucket.
sensitivityA value is out of range high when it is above quantile * (1 + margin) and above mean + z * sd. Only requests is also checked low. hold windows in a row raise the signal.
persistHow often the model is written to the runtime store, so a restart or another instance keeps what was learned.
MetricOne value per window
cpu, loop, memoryMean CPU share, highest loop delay p99 and last memory share from resources. Skipped on serverless.
requestsRequests per minute from the request counters.
errors, latency5xx share and p95 in ms; skipped under 20 requests in the window.
auth-failuresRefused sign-ins per minute, counted from auth.failed security events when the app has a securityEvents feed; skipped with MAIN-NORMS-0002 without one.
denials, egress-refusedAccess denials per minute from mainsdk:access.decision and enforced refusals per minute from mainsdk:network.decision. Counts only: no user, tenant or address is kept.

Calls

CallWhat it does
norms.get(metric, { at? })The bucket for now, or for a time: count, mean, sd, p50, p95, p99, learning and the last value.
norms.list({ cursor?, limit? })One view per configured metric, for now.
norms.delete(metric, { reason })Forget one metric's model, or "all". Needs norms.manage and is audited.

Signals

  • anomaly.detected (subject metric:<name>): value, expected (mean, sd, p50, p99), bucket, score, direction (high or low), learning. Severity is high from a score of 8, medium from 6, else low, and info while learning. It is sent again each minute while the anomaly holds.
  • anomaly.cleared after hold windows back in range.
  • pressure.contention when loop delay is high while your own CPU is at or under its median: the instance is waiting on something else on the machine.

Helpers for wiring and tests

CallWhat it does
new Norms(opts)The learner of one app instance, for tests and custom wiring.
parseNorms(section?)Read the norms section of main.yaml into settings.
normsIssues(section)List what is wrong in the norms section, without throwing.

What you get

  • A learner that is plain arithmetic: a decayed mean and variance and a decayed 48-bin histogram per hour of the week.
  • A value out of range raises one signal with the value, the expected range and a score, and a playbook, a person or the advisor can then act.
  • It is free.
  • The same input gives the same model and the same signals.
  • No dependency, no model and no service.
  • Holds numbers only, never a person, a tenant or an address.
  • Reads counters the SDK already keeps once a window, off the request path. A store that does not answer leaves the model in memory.

Errors and checks

  • MAIN-NORMS-0001: a norms value is invalid (mainsdk check).
  • MAIN-NORMS-0002: a metric's source is not available; it is skipped, logged once.
  • MAIN-NORMS-0003: the stored model was another version or corrupt; it was discarded and learning restarted.
  • MAIN-NORMS-0004: norms.delete refused, the caller lacks norms.manage (403).

Each code's cause and fix is in check codes.

Swapping drivers

norms has no drivers: the learner is built in, and persist sets how often the model is written to the runtime store.

Testing

createSim in @mainsdk/testing plays hours of traffic and CPU curves against an app in a second, on a fake clock, so you can assert what norms noticed. See testing.

bash
npx mainsdk norms          # each metric's bucket, count, p50, p99 and learning state
npx mainsdk info --json    # the same under "norms"

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet