inspector:watch

April 29, 2026 · View on GitHub

inspector:watch monitors PHP processes and automatically triggers profiling actions when configurable conditions are met.

Unlike inspector:top (real-time display) or inspector:daemon (continuous tracing), this command specializes in passive monitoring that only takes action when triggers fire, making it suitable for low-overhead production monitoring.

Quick Start

# Monitor a single process: dump memory when usage exceeds 256M
reli inspector:watch -p <pid> --memory-usage=256M

# Monitor multiple processes matching a regex
reli inspector:watch --target-regex="php-fpm" --memory-usage=512M

# Watch for a specific function appearing in the call stack
reli inspector:watch -p <pid> --watch-function="App\Service::heavyProcess"

# Grab 3 dumps and stop (ad-hoc debugging)
reli inspector:watch -p <pid> --memory-usage=128M --oneshot=3

Requirements

See Getting started § Requirements for the common runtime and target requirements.

Command-specific notes:

  • For --target-regex (daemon mode): same as inspector:daemon.

Triggers

Triggers define when to take action. At least one trigger must be specified.

Memory Limit (--memory-usage=<size>)

Fires when heap usage exceeds the threshold.

reli inspector:watch -p <pid> --memory-usage=256M
reli inspector:watch -p <pid> --memory-usage=1G

Memory Growth Rate (--memory-growth-rate=<size>/<period>)

Fires when memory grows faster than the specified rate. Useful for detecting memory leaks.

reli inspector:watch -p <pid> --memory-growth-rate=10M/min
reli inspector:watch -p <pid> --memory-growth-rate=500K/s

Supported periods: s (seconds), min (minutes), h (hours).

Memory Peak Watch (--memory-peak-watch)

Fires whenever the memory peak is updated. Useful for tracking peak memory progression.

reli inspector:watch -p <pid> --memory-peak-watch

Note: with exponential backoff cooldown, frequent peak updates are naturally throttled.

RSS Usage (--rss-usage=<size>)

Fires when the process's RSS (Resident Set Size) exceeds the threshold. Unlike --memory-usage which monitors PHP's internal heap, this monitors the actual physical memory used by the process as reported by the OS (/proc/[pid]/statm).

reli inspector:watch -p <pid> --rss-usage=512M
reli inspector:watch -p <pid> --rss-usage=1G

This is useful for detecting memory usage that occurs outside the Zend heap (e.g., FFI allocations, shared memory, memory-mapped files, or native extensions).

Function Detection (--watch-function=<name>)

Fires when the specified function appears in the call stack. Requires the fully qualified function name (exact match).

reli inspector:watch -p <pid> --watch-function="sleep"
reli inspector:watch -p <pid> --watch-function="App\Service::process"
reli inspector:watch -p <pid> --watch-function="PDO::query"

Trace Depth Limit (--trace-depth-limit=<N>)

Fires when the call stack exceeds N frames. Detects runaway recursion or deeply nested framework calls.

reli inspector:watch -p <pid> --trace-depth-limit=200

Variable Value (--watch-var=<expression>)

Fires when a PHP variable meets a condition. Multiple --watch-var flags can be specified.

Tip: To simply read a variable's current value without trigger conditions, use inspector:peek-var.

Syntax: scope::identifier:operator:value

Scopes

ScopeSyntaxWhat it reads
Globalglobal::$var$GLOBALS['var']
Locallocal::func()$varLocal variable in a specific function frame
Static propertystatic::Class::$propClass static property
Function staticfunc_static::func()$varFunction's static $var
Memorymemory::memory_get_usageZend MM heap stats

For local:: and func_static::, the function name is required. Use <main> for the top-level script scope.

The memory:: scope exposes memory_get_usage() / memory_get_peak_usage() equivalents as integers (bytes). Available names: memory_get_usage, memory_get_peak_usage, memory_get_usage_real, memory_get_peak_usage_real. See docs/inspection/peek-var-command.md for details.

Operators

OperatorTypesDescription
eq, neAllEqual / not equal
gt, lt, gte, lteint, floatNumeric comparison
containsstringSubstring match
count_gt, count_lt, count_eqarrayArray element count
is_nullAllCheck if null

Nested Access

Variable names support path expressions for nested array keys and object properties:

# Array key access
--watch-var='global::$config[database][pool_size]:gt:50'

# Object property access
--watch-var='global::$app->cache->size:gt:10000'

# Mixed
--watch-var='global::$container->services[cache]->pool[active]:gt:100'

Examples

# Global array growing too large
--watch-var='global::$cache:count_gt:10000'

# Local variable in a specific function
--watch-var='local::App\Controller::index()$response->statusCode:eq:500'

# Class static property
--watch-var='static::App\Cache::$entries:count_gt:50000'

# Function static variable (reads runtime value, not initial)
--watch-var='func_static::App\retry()$attempts:gt:10'

# Top-level script variable
--watch-var='local::<main>()$counter:gt:1000'

# Memory usage exceeds 100MB
--watch-var='memory::memory_get_usage:gt:104857600'

# Peak memory exceeds 256MB
--watch-var='memory::memory_get_peak_usage:gt:268435456'

CPU Usage (--cpu-usage=<percent>)

Fires when the process's CPU usage exceeds the threshold. Reads from /proc/[pid]/stat (user + system time).

reli inspector:watch -p <pid> --cpu-usage=80

Supports hysteresis to prevent rapid toggling around a single boundary:

# Enter when CPU >= 80%, exit when CPU < 60%
reli inspector:watch -p <pid> --cpu-usage=80 --cpu-usage-exit=60

Supports sustain duration — the condition must hold continuously before entering:

# CPU must stay >= 80% for 5 seconds before triggering
reli inspector:watch -p <pid> --cpu-usage=80 --cpu-sustain=5

Note: the first poll always returns null (needs two samples to compute delta), so the trigger never fires on the very first poll.

Sampling window: CPU% is calculated over a minimum 0.5-second window regardless of --poll-interval or --trace-interval. When tracing at 10ms intervals, the CPU% still reflects the average over the last 0.5s, preventing noisy readings from causing premature exit transitions.

Combining Triggers

Multiple triggers can be active simultaneously. When multiple triggers fire in the same poll cycle, actions execute once with a merged event:

reli inspector:watch -p <pid> \
  --memory-usage=256M \
  --memory-peak-watch \
  --watch-function="sleep" \
  --action=log

Output: [TRIGGERED] PID=1234 | trigger=memory-usage+memory-peak-watch | ...

Actions

Actions define what to do when a trigger fires.

There are three categories:

CategoryActionsWhen
One-shotmemory-dump, trace-once, log, execFires each time (with cooldown)
Stateful starttraceStarts continuous trace recording
Stateful stopstop-traceStops continuous trace recording

--action (Regular Actions)

Regular actions fire while the trigger condition is active, subject to cooldown. Default: memory-dump.

--on-enter / --on-exit (Lifecycle Actions)

Lifecycle actions fire exactly once on state transitions:

  • --on-enter=<action>: fires when the trigger condition becomes true
  • --on-exit=<action>: fires when the trigger condition becomes false

Both are repeatable. Combine with --action for mixed behavior:

# On CPU spike: start tracing + log; on recovery: stop tracing + log
reli inspector:watch -p <pid> \
  --cpu-usage=80 --cpu-usage-exit=60 --cpu-sustain=5 \
  --on-enter=trace --on-enter=log \
  --on-exit=stop-trace --on-exit=log \
  --trace-interval=10
# Mixed: start tracing on enter, also take memory dumps while active
reli inspector:watch -p <pid> \
  --cpu-usage=80 --cpu-usage-exit=60 \
  --on-enter=trace \
  --on-exit=stop-trace \
  --action=memory-dump

Memory Dump (--action=memory-dump)

Captures a binary memory dump (same .rdump format as inspector:memory:dump) when the trigger fires. The action only writes the raw dump — the watch loop deliberately does not run analysis inline (to keep the per-trigger stop time short and to keep cooldown / rate-limit semantics meaningful). Run inspector:memory:analyze later to produce a .rmem snapshot consumable by rmem:explore, inspector:memory:report, etc.

# 1. Watch fires and writes raw dumps:
reli inspector:watch -p <pid> --memory-usage=256M --action=memory-dump \
  --action-output-dir=/tmp/reli-dumps

# 2. Analyse offline (any time later, on any host):
reli inspector:memory:analyze /tmp/reli-dumps/watch-<pid>-<timestamp>.rdump \
  -f rmem -o snapshot.rmem
reli inspector:memory:report snapshot.rmem
# or: reli rmem:explore snapshot.rmem

Output files: <action-output-dir>/watch-<pid>-<timestamp>.rdump.

Trace Snapshot (--action=trace-once)

Outputs the call trace at the moment the trigger fires (one-shot).

reli inspector:watch -p <pid> --watch-function="sleep" --action=trace-once

--action accepts trace-once for a one-shot snapshot; plain trace is not a valid value here. For continuous recording, use --on-enter=trace with --on-exit=stop-trace (see below).

Continuous Trace (--on-enter=trace / --on-exit=stop-trace)

Starts/stops a continuous .rbt trace recording that samples the call stack at --trace-interval frequency while the trigger condition holds.

reli inspector:watch -p <pid> \
  --cpu-usage=80 --cpu-usage-exit=60 \
  --on-enter=trace --on-exit=stop-trace \
  --trace-interval=10

Output files: <output-dir>/watch-trace-<pid>-<timestamp>.rbt

During continuous tracing, the poll interval automatically switches to --trace-interval (default: 10ms) for higher sampling resolution. When tracing stops, it reverts to --poll-interval.

The trace session can coexist with other actions — memory dumps, log events, and exec commands continue to work during tracing.

In daemon mode, each worker manages its own trace session locally. Trace files are written to --action-output-dir with per-PID filenames. The controller receives notifications when traces start and stop.

Event Log (--action=log)

Logs trigger events with timestamp, PID, trigger name, and memory stats.

reli inspector:watch -p <pid> --memory-usage=256M --action=log --log-file=/var/log/reli-watch.log

Log format:

[2026-03-30T12:34:56+00:00] PID=1234 trigger=memory-usage mem=261.3M>256M mem=261.3M peak=261.3M

External Command (--action=exec)

Executes an external command (fire-and-forget, non-blocking). Context is passed via environment variables.

reli inspector:watch -p <pid> --memory-usage=256M \
  --action=exec \
  --action-exec-command='curl -s -X POST https://hooks.example.com/alert'
Environment VariableDescription
RELI_WATCH_PIDTarget process PID
RELI_WATCH_TRIGGERTrigger name(s)
RELI_WATCH_MEMORY_USAGECurrent memory usage (bytes)
RELI_WATCH_MEMORY_PEAKMemory peak (bytes)
RELI_WATCH_TIMESTAMPISO 8601 timestamp
RELI_WATCH_DUMP_PATHDump file path (if memory-dump action ran)

Multiple Actions

Actions can be combined:

reli inspector:watch -p <pid> --memory-usage=256M \
  --action=memory-dump --action=log --action=exec \
  --action-exec-command='notify-send "Memory alert"' \
  --log-file=/var/log/reli.log

Rate Limiting & Disk Protection

Cooldown (--cooldown=<seconds>)

Minimum wait time before the same trigger fires again. Default: 60 seconds.

With consecutive fires, the cooldown increases exponentially:

  • 1st fire: immediate
  • 2nd: 60s wait
  • 3rd: 120s wait (60 × 2)
  • 4th: 240s wait (60 × 2²)
  • Capped at --backoff-max (default: 3600s)

Resets when the trigger condition is no longer met.

--cooldown=30 --backoff-multiplier=2.0 --backoff-max=3600

Hourly Rate Limit (--max-triggers-per-hour=<N>)

Maximum trigger fires per hour (sliding window). Default: 10.

Disk Size Limit (--max-dump-size=<size>)

Maximum cumulative size of dump files. Default: 1G. Scans existing watch-*.rdump files in the output directory on startup, so the limit persists across restarts.

--max-dump-size=2G --action-output-dir=/var/log/reli/dumps

Oneshot Mode (--oneshot=<N>)

Capture N trigger events then exit. Alias for --max-triggers.

# Grab 5 memory dumps and stop
reli inspector:watch -p <pid> --memory-usage=128M --oneshot=5

Note: --oneshot is for ad-hoc debugging. For long-running daemons, use --max-dump-size + --max-triggers-per-hour + --cooldown instead — these persist across process restarts.

Daemon Mode (--target-regex)

Monitor multiple processes simultaneously:

reli inspector:watch --target-regex="php-fpm" \
  --memory-usage=512M \
  --action=log \
  --log-file=/var/log/reli-watch.log

Each discovered process gets its own worker that independently reads heap stats, evaluates triggers, and applies cooldown. Only trigger events are sent back to the controller. The --memory-limit option is propagated to each worker process.

Global --max-triggers (or --oneshot) is a single counter across all workers.

Output

[watch-daemon] Monitoring processes matching "{php-fpm}" | triggers=memory-usage | workers=8
[+process] PID=1234 assigned to worker 0
[+process] PID=2345 assigned to worker 1
[TRIGGERED] PID=1234 | trigger=memory-usage | mem=523.4M>512M (1/unlimited)
[-process] PID=2345 detached from worker 1

All Options

OptionDefaultDescription
-p, --pidTarget process PID (single-process mode)
-P, --target-regexRegex to find target processes (daemon mode)
--memory-usageTrigger on heap usage threshold
--memory-growth-rateTrigger on memory growth rate
--memory-peak-watchoffTrigger on peak memory update
--rss-usageTrigger on RSS (Resident Set Size) threshold
--watch-functionTrigger on function in call stack
--trace-depth-limitTrigger on call stack depth
--watch-varTrigger on variable value condition (repeatable)
--cpu-usageTrigger on process CPU usage (percent)
--cpu-usage-exitsame as enterCPU exit threshold (hysteresis)
--cpu-sustain0Seconds CPU must stay above threshold
--actionmemory-dumpRegular actions while active (repeatable)
--on-enterActions on enter transition (repeatable)
--on-exitActions on exit transition (repeatable)
--trace-interval10Sampling interval (ms) during tracing
--action-exec-commandCommand for exec action
--action-output-dir.Output directory for dumps and logs
--log-filestderrLog file path for log action
--poll-interval1000Polling interval in ms (min: 100)
--cooldown60Cooldown seconds between trigger fires
--backoff-multiplier2.0Exponential backoff multiplier
--backoff-max3600Maximum backoff seconds
--max-triggers-per-hour10Hourly trigger rate limit
--max-dump-size1GCumulative dump file size limit
--max-triggers0Total trigger limit (0=unlimited)
--oneshotAlias for --max-triggers
--memory-limitSet PHP memory_limit (e.g. 2G, 512M)
--quiet-watchoffSuppress terminal status output
-S, --stop-processoffStop target with ptrace during reads
-T, --threads8Worker count (daemon mode)
--no-cacheoffDisable binary analysis cache

Container Deployment

Kubernetes (Sidecar)

spec:
  shareProcessNamespace: true
  containers:
  - name: app
    image: php-app:latest
  - name: reli-watch
    image: reli-prof:latest
    command: ["reli", "inspector:watch",
      "--target-regex=php-fpm",
      "--memory-usage=512M",
      "--action=memory-dump", "--action=log",
      "--log-file=/var/log/reli/watch.log",
      "--action-output-dir=/var/log/reli/dumps/",
      "--max-dump-size=2G", "--quiet-watch"]
    securityContext:
      capabilities:
        add: ["SYS_PTRACE"]

Amazon ECS

{
  "pidMode": "task",
  "containerDefinitions": [{
    "name": "reli-watch",
    "essential": false,
    "linuxParameters": {"capabilities": {"add": ["SYS_PTRACE"]}}
  }]
}

Performance

At the default --poll-interval=1000 (1 second), per-target polling overhead is negligible for typical trigger combinations. Roughly: memory / RSS and function / depth triggers stay sub-millisecond; variable watch is the expensive tier, but still small at the default interval. In daemon mode the total cost scales with the number of monitored processes.

Measured polling overhead (PHP 8.4, median)
ConfigurationLatencyMax polls/sec
Memory triggers only~680μs~1,470
+ function / depth~750μs~1,330
+ variable watch~2,170μs~460

Trigger evaluation itself is < 1μs; cost is dominated by process_vm_readv calls. Even the heaviest configuration uses ~0.2% of a 1s polling interval.

See docs/internals/watch-command-architecture.md for the per-tier breakdown.