Skip to content

Checks In Depth

All NSClient++ checks — check_cpu, check_drivesize, check_service, check_eventlog, and dozens more — share one common engine. Once you understand it, you can configure any check.

This page walks through that engine from the ground up. Read it top-to-bottom the first time; later, jump to the section you need.

New to NSClient++?

Try the Quick Start or a Monitoring Scenario first to get a feel for what checks look like in practice.


1. How a Check Works

Every check follows the same five steps:

flowchart LR
    A[Collect items] --> B[Filter]
    B --> C[Evaluate<br/>warn / crit]
    C --> D[Format<br/>top / detail-syntax]
    D --> E[Return<br/>status + perfdata]
  1. Collect — gather a list of items (CPU cores, drives, services, event log entries…).
  2. Filter — keep only items matching the filter expression.
  3. Evaluate — test each kept item against warn and crit.
  4. Format — build the message using top-syntax and detail-syntax.
  5. Return — return the worst status, the message, and the performance data.

Each step has its own option. The four most important are:

Option Purpose
filter Which items are included
warn / crit Which items trigger an alert
top-syntax / detail-syntax / ok-syntax What the message looks like
perf-config / perf-syntax What performance data looks like

2. Trying It Out (Test Mode)

The fastest way to learn the engine is to drive a check yourself. NSClient++ ships with a built-in test shell that runs the same modules the service does, but interactively, with all log output visible.

2.1. Start the shell

nscp test --settings dummy
...
L     client Enter command to inject or exit to terminate...

--settings dummy skips your real configuration so nothing on disk surprises you. Type exit to leave.

Trace and debug log lines are hidden by default. Add --log debug (or --log trace for maximum verbosity, including incoming requests and child-process spawn/exit) when you need to see them:

nscp test --settings dummy --log debug

2.2. Load a module

Out of the box the shell has no checks loaded. Each check lives in a module — load it once per session, then run the check:

load CheckSystem

2.3. Run a check

Now we can run the check:

check_cpu
L     client OK: CPU Load ok
L     client  Performance data: 'total 5m'=0%;80;90 'total 1m'=1%;80;90 'total 5s'=11%;80;90

Options are passed as keyword=value, with quotes when the value contains spaces or shell-meta characters. Some keywords are flags with no value (e.g. help, show-all, show-default):

check_cpu filter=none
check_cpu "filter=core = 'total'" "warn=load > 80"

2.4. Two essential helpers: show-default and help

Before changing anything, learn these two — they work for every check.

show-default prints the check’s built-in options so you can learn what it does by default and copy a single line to tweak:

check_cpu show-default
"filter=core = 'total'" "warning=load > 80" "critical=load > 90"
"empty-state=ignored" "top-syntax=${status}: ${problem_list}"
"ok-syntax=%(status): CPU load is ok." "detail-syntax=${time}: ${load}%"
"perf-syntax=${core} ${time}"

help gives the full list of options, filter keywords, and per-check examples:

check_cpu help

Note

You’ll see ${name} in the defaults output. That’s the older placeholder syntax, still supported for compatibility. When you write your own configuration, prefer %(name) — see section 5.

Warning

Don’t paste all defaults into your config. Defaults can change in newer versions; pinning them removes that benefit.

2.5. A worked example

The same check_cpu, with progressively more customisation:

# Defaults
check_cpu
OK: CPU load is ok.
'total 5m'=2%;80;90 'total 1m'=5%;80;90 'total 5s'=11%;80;90

# Custom thresholds
check_cpu "warn=load > 50" "crit=load > 70"
OK: CPU load is ok.
'total 5m'=2%;50;70 ...

# Per-core data instead of just the total
check_cpu filter=none "warn=load > 80" "crit=load > 90"

# Custom message
check_cpu "top-syntax=%(status): CPU usage is %(list)" \
          "detail-syntax=%(time) avg: %(load)%"
OK: CPU usage is 5m avg: 2%, 1m avg: 5%, 5s avg: 11%

The rest of this page explains exactly what each of those options does.


3. Filters — Choosing What to Check

A filter is an expression evaluated for each item. Items where it is true are included; items where it is false are dropped.

If you don’t supply one, the check uses its default (e.g., check_cpu defaults to core = 'total').

Syntax

keyword operator value

Combine with and, or, not. Disable the default with filter=none.

check_cpu "filter=core = 'total'"
check_cpu "filter=load > 5 or core = 'total'"
check_service "filter=start_type = 'auto' and not name like 'clr_optimization'"
check_cpu filter=none                       # include everything

Operators

Symbol Safe alias Meaning
= eq Equals
!= ne Not equals
> < >= <= gt lt ge le Numeric comparison
like — Substring match
regexp — Regular expression (full match — wrap in .* to search)
in — Membership in a list
and or not — Logical
'...' str(...) String literal

Note

Use the safe aliases (gt, lt, …) when passing arguments through NRPE or shells — they avoid </> redirection problems. The same expression language powers filter, warn, crit, and the %(...) placeholders in top-syntax / detail-syntax, so anything you learn here applies everywhere.

Common keywords

These are available in every check:

Keyword Meaning
count Items matching the filter
total Items before filtering
ok_count / warn_count / crit_count Items in each state
problem_count Warning + critical
status Current overall status

Each check adds its own keywords. For check_cpu: core, core_id, load, idle, kernel, time. Use <check> help to discover them.

Practical filter recipes

# CPU: only the aggregate total
check_cpu "filter=core = 'total'"

# CPU: cores actually doing work, plus the total
check_cpu filter=none "filter=load > 5 or core = 'total'"

# Services: only auto-start, exclude one by name
check_service "filter=start_type = 'auto' and name != 'Spooler'"

# Event log: errors and criticals only
check_eventlog "filter=level in ('error', 'critical')"

# Disk: fixed and network drives
check_drivesize drive=* "filter=type in ('fixed', 'remote')"

Size and time units

Suffix Size Time
k / m / g / t KB / MB / GB / TB —
s / m / h / d / w — seconds / minutes / hours / days / weeks
check_memory "warn=free < 4g"
check_uptime "warn=uptime < 1d"

Past times use negative values — -1h means “less than an hour ago”:

check_eventlog scan-range=-24h "crit=written > -1h"

4. Thresholds — Choosing What’s a Problem

warn and crit use the same expression language as filters — same operators, same keywords, same and/or/not. The difference is what the expression decides:

Expression Per-item question Effect when true
filter Should I include this item at all? Item is kept (or dropped)
warn Is this item a warning? Overall status becomes WARNING
crit Is this item critical? Overall status becomes CRITICAL

Aggregation rules:

  • Any item matches crit → result is CRITICAL
  • Any item matches warn (and none matched crit) → WARNING
  • Otherwise → OK
check_cpu "warn=load > 80" "crit=load > 90"
check_memory "warn=free < 20%" "crit=free < 10%"
check_drivesize "warn=free < 15%" "crit=free < 5%"

Disabling a threshold

check_cpu warning=none
check_cpu critical=none

Composite thresholds

# Warn on high kernel time OR high load
check_cpu filter=none "warn=kernel > 10 or load > 80" "crit=load > 90"

# Warn only on big machines that are tight on memory
check_memory "warn=free < 4g and size > 16g"

Aggregate thresholds

The count family of keywords lets you alert on totals rather than individual items:

# Alert if more than 3 services are stopped
check_service "crit=problem_count > 3"

# Alert if any matching event was written in the last hour
check_eventlog scan-range=-1w "crit=written > -1h"

empty-state — when nothing matches

What status to return when the filter selects no items:

Value Meaning
ok Return OK (default for most checks)
warning Return WARNING
critical Return CRITICAL
ignored Suppress the result
check_service "filter=name = 'NonExistentService'" empty-state=ok

empty-state is about a filter that matched nothing. A check whose background collector has not produced any data yet is a different situation with its own option, warmup-state — see section 8.


5. Output Syntax — Choosing the Message Text

Three options shape the message. They affect only the human-readable text — never the status or perfdata.

Render what you filtered on

The filter decides the state, the syntax decides the text, and the two share nothing. A threshold on a keyword the detail-syntax does not mention flips the check to WARNING or CRITICAL while the message still reports whatever the default line happens to carry — for example check_tcp ... "crit=cert_issuer_cn != 'R11'" goes CRITICAL but still reads www.example.com:443 ok in 10ms, because the default detail line renders only result and time. Whenever you threshold on something the default message does not show, add it: "detail-syntax=%(host):%(port) issuer=%(cert_issuer_cn)".

Option When applied Default purpose
top-syntax Always — the whole message Status + list of problems
detail-syntax Per item, inside the list Per-item values
ok-syntax When status is OK Brief “all ok” message
list-separator Between the items of a list ,

check_cpu defaults (still using the legacy ${...} form for top-syntax / detail-syntax; written the recommended way they’d be top-syntax=%(status): %(problem_list) and detail-syntax=%(time): %(load)%):

"top-syntax=${status}: ${problem_list}"
"ok-syntax=%(status): CPU load is ok."
"detail-syntax=${time}: ${load}%"

Template variables

NSClient++ has two placeholder forms. Use %(name) — it’s the modern, more capable syntax.

Form Status When it makes sense
%(name) Preferred Always. Survives shells/NRPE, supports nested parens and function calls.
${name} Legacy Still works for plain variable references; kept for backwards compatibility.

Differences in practice:

  • Plain variable references — both forms work and produce identical output.
  • Function calls (see section 6) — only %(...) works. The ${...} form stops at the first } and can’t capture nested parentheses.
  • Shells and NRPE — ${...} is often eaten by Bash and similar shells before it reaches NSClient++. %(...) passes through untouched.

The rest of this document uses %(...) in every example. ${...} is documented only where it appears in defaults so you can recognise it in older configs.

Common variables (every check):

Variable Meaning
%(status) OK / WARNING / CRITICAL / UNKNOWN
%(list) All filtered items, joined with detail-syntax
%(problem_list) Only warning/critical items
%(ok_list) / %(warn_list) / %(crit_list) Items in each state
%(count) / %(problem_count) Item counts
%(sep) The decoded list-separator (for line breaks)

Per-item variables (in detail-syntax) depend on the check — <check> help lists them.

Recipes

# Show all values, not just problems
check_cpu show-all
# equivalent to:
check_cpu "top-syntax=%(status): %(list)"

# Custom CPU message
check_cpu time=5m \
  "top-syntax=%(status): Cpu usage is %(list)" "detail-syntax=%(load)%"
OK: Cpu usage is 26%

# Custom memory message
check_memory "top-syntax=%(list)" \
  "detail-syntax=%(type) free: %(free) used: %(used) size: %(size)"
page free: 16G used: 7.98G size: 24G, physical free: 4.18G used: 7.8G size: 12G

# Service: name and state for each
check_service "top-syntax=%(list)" "detail-syntax=%(name): %(state)"

Multi-line output

A check that matches many items produces one long line, because list items are joined with ,. list-separator changes what joins them. Its value accepts the escapes \n, \r, \t and \\ — a configuration file value is a single line, so a real newline cannot be written into one. In an alias the backslash itself has to be doubled ("list-separator=\\n"), because the alias definition is parsed first and refuses a bare \n.

The templates themselves are never escape-decoded: a top-syntax containing C:\temp must keep its literal backslashes. To break the line before the first item, reference the decoded separator as %(sep) in the template; the separator itself breaks between the rest.

check_users "top-syntax=%(status): %(count) user(s) logged on:%(sep)%(list)" \
  "detail-syntax=%(user) [%(state)]" "list-separator=\n"
OK: 7 user(s) logged on:
administrator [active]
user1 [active]
user2 [active]

Nagios-compatible frontends (Icinga, Naemon, …) treat the first line as the summary and the rest as long output, shown as a block — which is the point of the exercise.

Where the performance data ends up

Results go on the wire as message|perfdata, so with a multi-line message the perfdata lands at the end of the last line. That is valid plugin output — the plugin API allows perfdata on the final long-output line — and Icinga 2 parses it, but a consumer that only looks at the first line will show it as text. Check yours before rolling this out fleet-wide.

Not for line-oriented transports

Embedded newlines break protocols that treat one line as one result: check_mk local checks and collectd in particular. NRPE and NSCA carry them, but both truncate at a fixed payload size, so a long multi-line result is cut off sooner than the single-line form would be.

Number formatting

By default every byte value scales on its own and carries up to three decimals, which is how a single line ends up reading 141.085GB/0.983TB — two units, six decimals, one disk. Four options change that, and they are available on every filter check:

Option Effect Default
decimals Decimals to render, exactly (2 → 25.20) -1 (up to 3)
byte-unit Pin every byte value to one unit: B…EB auto per value
decimal-separator Radix character — , for the European rendering .
thousands-separator Digit grouping for the integer part none
check_drivesize drive=/ show-all=true
OK /: 141.085GB/0.983TB used

check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB
OK /: 141.09GB/1006.85GB used

check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB decimal-separator=, thousands-separator=.
OK /: 141,09GB/1.006,85GB used

They apply to the byte and percentage keywords and to the functions below. Setting any of them also switches plain float keywords in the message templates over to the number format: instead of the legacy 6-significant-digit rendering (which goes scientific past a million, 1.23457e+07), floats then render with the configured decimals — or up to three decimals with trailing zeros stripped while decimals is unset. Strings, integers, durations and dates never change.

Only the message changes

Performance data is generated from the raw values, so it keeps its full precision and its . radix character whatever you set here — graphs and time series are unaffected. The numbers you write in a filter, warning or critical are parsed by the filter language and always use . as well: warning=used > 1.5g means the same thing with decimal-separator=, in force.

Setting it once

These are ordinary check options, so an alias fixes them for a whole command: [/settings/external scripts/alias] alias_disk = check_drivesize decimals=2 byte-unit=GB. Real-time filters take the same values as the decimals, byte unit, decimal separator and thousands separator settings keys, which inherit from the default template.


6. Functions — Transforming Values

Sometimes a raw value isn’t what you want to look at. A counter that returns bytes per second is unfriendly to read as 4194304, and a threshold expressed in MB is easier to maintain than one expressed in 1024-scaled bytes. NSClient++ supports functions for these jobs, and the same function works in both contexts:

  • inside detail-syntax / top-syntax to format a value for display
  • inside warn / crit / filter to derive a value for comparison

Calling a function

A function call looks like a normal function: name(arg1, arg2, …). Arguments can mix variables, string literals, and numbers. Always wrap the call in %(...) when using it inside a syntax template:

detail-syntax = "Used: %(format_bytes(used))"
warning       = "convert_bytes(used, 'MB') > 500"
filter        = "scale(rate, 1000000) > 100"

Function calls require %(...)

The legacy ${...} placeholder stops at the first } and can’t capture nested parentheses, so ${format_bytes(used)} won’t parse. Use %(...) for function calls (and prefer it everywhere else — see section 5).

Built-in functions

These are available wherever the check exposes them — every check with byte-valued keywords does, including check_drivesize, check_memory, check_network, check_pdh and the disk checks. Run <check> help to see the list for a given check.

Function Returns Purpose
format_bytes(value) string Auto-scaled human-readable bytes — 4194304 → "4MB" (1024-based)
format_bytes(value, 'MB') string Fixed unit, without the suffix. Units: B, KB, MB, GB, TB, …
format_bytes(value, 'MB', 1) string The same with exactly one decimal
format_number(value, 2) string Any number with a fixed number of decimals — percentages, rates, …
convert_bytes(value, 'MB') float Numeric value in the named unit — use in thresholds
scale(value, divisor) float Divide by an arbitrary divisor — for decimal units (Mbps, etc.)

Unit names are case insensitive (GB, gb and g all mean the same thing) and a unit that names nothing is reported rather than rendered: format_bytes(used, 'ZB') makes the check return UNKNOWN with Unknown byte unit: ZB. Without an explicit decimals argument these follow the check’s number formatting options.

Recipes

# Show raw bytes as MB/GB/etc. in the message
check_pdh counter=disk_bytes \
  "detail-syntax=%(alias) is %(format_bytes(value))"

# Threshold in MB, display in human-friendly units
check_pdh counter=memory_bytes \
  "warning=convert_bytes(value, 'MB') > 500" \
  "detail-syntax=%(alias) = %(format_bytes(value))"

# Network rates — Mbps is decimal (10⁶), use scale()
check_pdh counter=bytes_per_sec \
  "detail-syntax=Speed = %(scale(value, 1000000)) Mbps"

# Combine functions and plain variables in one template
check_pdh counter=disk_writes \
  "detail-syntax=%(counter): %(format_bytes(value)) (%(value) raw bytes)"

How it composes with the rest of the language

Functions are first-class values, so the result composes with operators, and/or, and not:

# Warn if used MB AND free MB are both at risk
check_disk \
  "warn=convert_bytes(used, 'MB') > 800 and convert_bytes(free, 'MB') < 100"

# Critical when ANY of two derived values is over budget
check_pdh ... \
  "crit=scale(read_rate, 1000000) > 50 or scale(write_rate, 1000000) > 50"

The function result has a regular type (string or float), so it slots into comparisons exactly like a plain variable reference. The arguments themselves can be variables, literals, or — for nested calls — other function calls: %(format_bytes(scale(value, 1024))) is valid.

Variable-style shortcuts

Some checks expose pre-scaled “view variables” for the most common cases — check_pdh provides value_human, value_mb, value_gb, etc. These are syntactic sugar for the corresponding function calls:

# Variable-style shortcut
check_pdh counter=mem "detail-syntax=Mem: %(value_human)"
check_pdh counter=mem "warning=value_mb > 500"

# Equivalent function calls
check_pdh counter=mem "detail-syntax=Mem: %(format_bytes(value))"
check_pdh counter=mem "warning=convert_bytes(value, 'MB') > 500"

Reach for variables when one of the prebuilt units fits; reach for functions when you need a custom unit, a custom divisor, or composition with other expressions.


7. Performance Data

Performance data is the machine-readable metrics used for graphing, in the standard Nagios format:

'metric_name'=value[unit];[warn];[crit];[min];[max]

Example:

'total 5m'=2%;80;90 'total 1m'=5%;80;90 'total 5s'=11%;80;90

Perfdata needs thresholds

Without warn/crit, perfdata values are emitted but empty:

check_cpu warning=none critical=none
'total 5m'= 'total 1m'= 'total 5s'=

To get values without alerts, use perf-config to mark metrics as “extra”:

check_cpu warning=none critical=none "perf-config=extra(load)"

Customising with perf-config

perf-config works like a tiny stylesheet — selectors target metrics, keys transform them.

"perf-config=selector(key:value; key:value) selector2(key:value)"
Key Effect
unit Force a unit (G, M, K, %, ms, …)
ignored true → drop this metric
prefix / suffix Rename parts of the metric name
minimum / min Force the perfdata min field (use min as a shorthand)
maximum / max Force the perfdata max field — useful for graphing systems that auto-fit

Selectors match in order of specificity: prefix.object.suffix → prefix.object → object.suffix → prefix → suffix → object. The * selector matches everything.

Recipes

# Lock memory metrics to GB (avoids graph jumps when auto-scaling switches units)
check_memory "perf-config=*(unit:G)"

# Lock disk metrics to GB
check_drivesize "perf-config=*(unit:G)"

# Drop the percent metrics from check_drivesize, keep the absolute ones
check_drivesize "perf-config=used %(ignored:true)"

# Rename: drop suffix label, force GB
check_drivesize "perf-config=used.used(unit:G;suffix:'') used %(ignored:true)"
'C:\'=213G;178;201;0;223 'D:\'=400G;372;419;0;465

# Force min/max bounds on a counter that doesn't know its own range
# (e.g. a raw PDH counter exposed by check_pdh). The graphing system can
# then auto-scale to the declared range instead of guessing from history.
check_pdh "counter=\\Processor(_Total)\\% Processor Time" \
  "perf-config=*(minimum:0;maximum:100)"

# Same idea on a custom queue-depth counter, with `min`/`max` shorthand.
check_pdh counter=queue_depth "perf-config=queue_depth(min:0;max:12345)"

Inspecting performance data

render_perf remove-perf command=check_drivesize
OK: OK:
C:\ used      213.605 GB      178.777 201.124 223.471 0
C:\ used %    95      %       79      89      100     0

perf-syntax — naming metrics

perf-syntax controls the metric name (not its value), using the same template variables as detail-syntax:

check_cpu "perf-syntax=%(core) %(time)"     # 'total 5m'=...
check_cpu "perf-syntax=%(core)_%(time)"     # 'total_5m'=...

Useful when your graphing system is picky about names.

How a metric name is built

A check has one primary metric — the one it is really about, which its default thresholds report — and that one is graphed under the bare alias perf-syntax renders. Every other keyword adds its own name: <perf-syntax>_<keyword>. check_cpu reports load, so a row aliased total 5m is graphed as 'total 5m'; asking the same check for user as well adds 'total 5m_user' rather than a second 'total 5m'.

The name a keyword is graphed under is therefore fixed: it does not change when the query mentions another keyword, so a graph template can rely on it. Override the pieces per keyword with perf-config:

check_cpu "perf-config=load(prefix:cpu_ suffix:_load)"   # 'cpu_total 5m_load'=...

8. Collector-Backed Checks and Warm-Up

Most checks read what they report at the moment you run them: a service’s state, a file’s size, the free space on a drive. A few cannot, because the value only exists as a difference or an average over time — CPU load, network throughput, disk I/O rates. For those, a background collector inside the module samples the system on a fixed interval and keeps a rolling buffer, and the check answers from that buffer.

Module Collector interval Checks that read it
CheckSystem (Windows) 1 second check_cpu, check_load, check_network, check_temperature, check_cpu_frequency, check_battery, check_os_updates, check_process_history, check_process delta=true
CheckSystemUnix 1 second check_cpu, check_memory, check_pagefile, check_network, check_process_history
CheckDisk 10 seconds check_disk_io, check_disk_health, the trend keywords of check_drivesize (full_in, rate, …)

Two things follow from this.

Time windows are capped at the agent’s uptime. check_cpu time=15m averages the samples of the last fifteen minutes — or, if the agent started five minutes ago, the five minutes it has. The check does not wait for the window to fill; it answers from what it has. The same goes for the 5- and 15-minute averages of check_load, and the check_drivesize trend keywords need several samples spread over a while before they produce a value (trend_samples and trend_span say how much they have, so you can threshold on that).

Before the first sample there is nothing to report. For the first moment after the agent (or the module) starts, the collector has not sampled anything yet. Answering from the empty buffer would report an idle CPU or zero traffic as if it had been measured, so instead the check says so:

check_cpu
UNKNOWN: No CPU data available yet (collector still initializing)

warmup-state — what to report during warm-up

UNKNOWN is the default because the check genuinely does not know. If a restart of the agent should not raise anything in your monitoring, pick a different status with warmup-state:

Value Meaning
unknown Return UNKNOWN (default)
ok Return OK
warning Return WARNING
critical Return CRITICAL
check_cpu warmup-state=ok
OK: No CPU data available yet (collector still initializing)

The message stays the same whatever status you choose, so a warm-up result is never mistaken for a reading. The option only covers the time before the first sample: once there is data, the check is evaluated normally and warmup-state has no effect. It takes the same spellings as the other status options (warn and crit work too), and an unrecognised value is rejected whenever the check runs, not saved for the next restart. Any problem with the options themselves — an invalid time=, a typo — is reported before the collector is even consulted, so it never hides behind a warm-up answer.

Warming up versus not working

warmup-state only ever applies to a collector that has not tried yet. Everything else that leaves a check without data is a fault, and is reported UNKNOWN whatever warmup-state says — so warmup-state=ok can quiet a restart, but never a broken collector:

Situation Result
The collector has not taken its first sample yet warmup-state (UNKNOWN by default), “… (collector still initializing)”
The collector has tried and every attempt failed (e.g. /proc unreadable) UNKNOWN “No … data available: the collector failed to sample it: “
It worked, but its last five samples in a row failed The same UNKNOWN: the older samples still in the buffer are not reported as current
It failed, and has just become readable again (CPU and network, one tick) UNKNOWN “… could read it again just now, after failing; the first sample follows next”
Sampling is switched off (disable = cpu, disable = load) UNKNOWN “… sampling is disabled”
The collector is running but its lock could not be had in time (Windows) UNKNOWN “… the collector is busy (timed out waiting for its lock)”

warmup-state is accepted by the checks that can tell “not sampled yet” apart from “no data”:

Platform Checks
Windows check_cpu, check_load
Linux and macOS check_cpu, check_memory, check_pagefile, check_network

Other checks reject it as an unknown option. On Windows, check_cpu also refuses an explicit warmup-state while use pdh for cpu is enabled: the PDH counters cannot tell a warm-up from a counter that is unavailable, so there is no warm-up to choose a status for.

Checks that cannot tell warm-up apart yet

Some collector-backed checks cannot yet distinguish “not sampled yet” from “nothing there”: on Windows check_network, check_temperature, check_cpu_frequency, check_battery, check_os_updates and check_process_history, and CheckDisk’s check_disk_io and check_disk_health. Right after a start these may answer with an empty result (through empty-state), with zero rates, or with “no sensors found”, rather than with a warm-up message. Give the agent a few seconds after a restart before trusting them.

Running checks right after a start

This matters most when the agent is started just to answer one query. nscp client --boot --query check_cpu loads the module, asks at once and exits — it always lands in the warm-up window. Run collector-backed checks against a running agent (the service, or nscp test), and when you script against a freshly started one, retry until the message no longer says initializing.

The same goes for anything that wraps nscp client and runs it once per check: a monitoring agent that runs nscp.exe client ... --query check_cpu as a local plugin (the Icinga 2 nscp-local-* check commands work this way), a scheduled task, or check_and_forward run from the command line. Each run starts a new process with an empty buffer, so a collector-backed check never gets past warm-up there, however often it runs. warmup-state=ok only hides that; it does not produce a reading. Ask the agent that is already running instead, over NRPE, the REST API (check_nscp_api), or NSCA/NRDP from its own scheduler. Point-in-time checks (check_drivesize, check_service, check_memory on Windows, …) are not affected and work fine one-shot.


9. Putting It Together

Pick a check, run show-default, identify the option you want to change, change just that one. Repeat.

# Default behaviour
check_drivesize

# Step 1 — only fixed disks
check_drivesize "filter=type = 'fixed'"

# Step 2 — tighter thresholds
check_drivesize "filter=type = 'fixed'" \
  "warn=free_pct < 15" "crit=free_pct < 5"

# Step 3 — clean message with human-readable sizes (functions from section 6)
check_drivesize "filter=type = 'fixed'" \
  "warn=free_pct < 15" "crit=free_pct < 5" \
  "top-syntax=%(status): %(list)" \
  "detail-syntax=%(drive_or_id) %(format_bytes(free)) free of %(format_bytes(size))"

# Step 4 — graph-friendly perfdata
check_drivesize "filter=type = 'fixed'" \
  "warn=free_pct < 15" "crit=free_pct < 5" \
  "perf-config=*(unit:G) used %(ignored:true)"