netflow-plugin
Rust NetFlow/IPFIX/sFlow ingestion and query plugin.
It stores flow entries in journal tiers under the Netdata cache directory and exposes
flows:netflow.
Offline Function test mode
Fixture harnesses can execute the Function query path directly against an existing NetFlow backend directory:
netflow-plugin --test flows:netflow --dir <flows-dir> [--timeout <seconds>] [--no-persist] < payload.json
Requirements:
<flows-dir>is the NetFlow backend root containing theraw,1m,5m, and1htier directories.- stdin is the JSON Function request body (non-empty, maximum 16 MiB).
--requestis not supported and fails with usage output.--timeout <seconds>controls the offline Function execution timeout. It defaults to30; use--timeout 0to map to a very large finite timeout for long-running fixture comparisons.- stdout contains only the raw JSON Function response.
- errors are written to stderr and return non-zero.
Use --no-persist for shared fixture datasets. It prevents the test run from writing
facet state or sidecar files under <flows-dir> while keeping facet data in memory for
the Function response. Without --no-persist, the plugin may refresh facet state under
the backend directory, matching normal runtime behavior.
Function output includes volatile fields such as collection timestamps and runtime statistics. Test harnesses should normalize those fields before comparing fixture outputs.
Configuration
When running under Netdata, config is loaded from netflow.yaml in:
${NETDATA_USER_CONFIG_DIR}/netflow.yaml(preferred)${NETDATA_STOCK_CONFIG_DIR}/netflow.yaml(fallback)
Top-level toggle:
enabled: true|falsecontrols the netflow plugin itself.
If journal.journal_dir is relative (default: flows), it is resolved against
NETDATA_CACHE_DIR. With the standard cache directory this becomes:
/var/cache/netdata/flows/raw/var/cache/netdata/flows/1m/var/cache/netdata/flows/5m/var/cache/netdata/flows/1h
If enrichment.geoip does not define explicit MMDB paths, the plugin auto-detects
packaged databases in this order:
${NETDATA_CACHE_DIR}/topology-ip-intel${NETDATA_STOCK_DATA_DIR}/topology-ip-intel
netdata-plugin-netflow now ships a stock MMDB seed set under
${NETDATA_STOCK_DATA_DIR}/topology-ip-intel. Freshly downloaded data written by
topology-ip-intel-downloader stays in ${NETDATA_CACHE_DIR}/topology-ip-intel and
overrides the stock copy automatically.
Important:
- packaged installs ship the stock MMDB payload
- source installs from a Git checkout do not include the generated stock MMDBs by default
- local/source installs should run
topology-ip-intel-downloaderif they want a local cache copy - packaged 32-bit installs still ship the stock MMDB payload, but do not include
topology-ip-intel-downloader
protocols.decapsulation_mode
Controls packet decapsulation for datalink payload parsing:
none(default): keep outer header view.srv6: enable SRv6 decapsulation for supported payloads.vxlan: enable VXLAN decapsulation for supported payloads.
protocols.timestamp_source
Controls which timestamp is written as _SOURCE_REALTIME_TIMESTAMP for decoded flows:
input(default): packet receive time at ingestion.netflow_packet: NetFlow/IPFIX packet export timestamp.netflow_first_switched: first-switched timestamp from flow fields when available.
enrichment (Akvorado-style static metadata and sampling)
Optional enrichment is applied at ingestion time. When enabled, it follows Akvorado core behavior for static metadata and sampling defaults/overrides:
- metadata lookup by exporter prefix + interface index
- sampling override/default by exporter prefix
- exporter/interface classifier rules (
exporter_classifiers,interface_classifiers) - classifier cache TTL (
classifier_cache_duration, default5m, minimum1s) - routing static lookup for AS/mask/next-hop/path/community fields (
routing_static) - routing dynamic lookup from BMP updates (
routing_dynamic.bmp) - BMP decode-error tolerance is configurable (
routing_dynamic.bmp.max_consecutive_decode_errors) - network attribute lookup by source/destination IP prefix (
networks) - GeoIP lookup from local MMDB files (
geoip) - periodic remote network source refresh (
network_sources) - network attribute merge order: GeoIP base, then remote
network_sources, then staticnetworks - static
networkssupernet/subnet inheritance (more specific prefixes override non-empty fields) - AS and net provider order controls (
asn_providers,net_providers) - flows are dropped when metadata/sampling requirements are not met
- when both static and dynamic routes match, dynamic routes are preferred
Example:
enrichment:
classifier_cache_duration: 5m
default_sampling_rate:
192.0.2.0/24: 1000
override_sampling_rate:
192.0.2.128/25: 4000
metadata_static:
exporters:
192.0.2.0/24:
name: edge-router
region: eu
role: peering
tenant: tenant-a
site: par
group: blue
default:
name: Default0
description: Default interface
speed: 1000
if_indexes:
10:
name: Gi10
description: 10th interface
speed: 1000
provider: transit-a
connectivity: transit
boundary: external
geoip:
asn_database:
- /usr/share/GeoIP/GeoLite2-ASN.mmdb
geo_database:
- /usr/share/GeoIP/GeoLite2-City.mmdb
optional: true
networks:
198.51.100.0/24:
name: customer-a
role: customer
site: par1
region: eu-west
country: FR
state: Ile-de-France
city: Paris
tenant: tenant-a
asn: 64500
203.0.113.0/24: transit-a
network_sources:
amazon:
url: "https://ip-ranges.amazonaws.com/ip-ranges.json"
method: GET
tls:
enable: true
verify: true
ca_file: ""
cert_file: ""
key_file: ""
interval: 10m
timeout: 30s
transform: |
(.prefixes + .ipv6_prefixes)[] |
{ prefix: (.ip_prefix // .ipv6_prefix), tenant: "amazon", region: .region, role: .service|ascii_downcase }
headers:
X-Example: "value"
asn_providers: [flow, routing, geoip]
net_providers: [flow, routing]
routing_static:
prefixes:
198.51.100.0/24:
asn: 64600
as_path: [64550, 64600]
communities: [123456, 654321]
large_communities:
- asn: 64600
local_data1: 7
local_data2: 8
next_hop: 203.0.113.9
routing_dynamic:
bmp:
enabled: true
listen: "0.0.0.0:10179"
receive_buffer: 0 # bytes; 0 keeps kernel default
max_consecutive_decode_errors: 8
rds: ["0", "65000:100", "192.0.2.1:42"]
collect_asns: true
collect_as_paths: true
collect_communities: true
keep: 5m
bioris:
enabled: false
ris_instances: []
timeout: 200ms
refresh: 30m
refresh_timeout: 10s
routing_dynamic.bioris is implemented as stream-based route ingestion:
- the plugin periodically discovers routers with
GetRouters - it consumes
DumpRIBstreams per router/AFI for baseline reconciliation - it keeps
ObserveRIBstreams per router/AFI for incremental updates between refreshes - enrichment keeps lookup local/in-memory (no per-flow remote RPC on the ingestion hot path)
network_sources.*.transform accepts jq expressions (compiled/executed via jaq) and
should emit objects with fields compatible with:
prefix(required)name,role,site,region,country,state,city,tenant,asn,asn_name(optional)
network_sources.*.tls follows Akvorado-style source TLS controls:
enabletoggles TLS settings for this source.verifymust remaintrue; disabling TLS certificate verification is not supported.skip_verifyis accepted only for compatibility with earlier drafts and must remainfalse.ca_filesets a custom CA bundle.cert_fileandkey_fileset optional client identity (ifkey_fileis empty,cert_fileis reused).
Example:
enabled: true
listener:
listen: "0.0.0.0:2055"
protocols:
v5: true
v7: true
v9: true
ipfix: true
sflow: true
decapsulation_mode: srv6
timestamp_source: input
journal:
journal_dir: flows
tiers:
raw:
size_of_journal_files: 200GB
duration_of_journal_files: 24h
minute_1:
size_of_journal_files: 40GB
duration_of_journal_files: 14d
minute_5:
size_of_journal_files: 30GB
duration_of_journal_files: 30d
hour_1:
size_of_journal_files: 20GB
duration_of_journal_files: 365d
query_max_groups: 50000
Standalone CLI runs still accept the legacy uniform retention flags
--netflow-retention-size-of-journal-files and
--netflow-retention-duration-of-journal-files. They apply the same value to
all tiers and exist only for standalone/CLI compatibility; YAML configuration is
per-tier.
query_max_groups caps the number of distinct group keys a single
aggregation query may build before extra groups are folded into a synthetic
__overflow__ bucket. The response carries a warning when this happens. The
limit protects the query worker from accidentally wide group-by combinations
exhausting memory.
Journal rotation size is not user-configured. The plugin derives it per tier:
- if
size_of_journal_filesis set, rotation size isclamp(size / 20, 5MB, 200MB) size_of_journal_filesmust be at least100MB- if
size_of_journal_filesis omitted or explicitly set tonull, the plugin uses a fixed internal rotation size of100MB - the internal time-based rotation cadence remains
1h
The plugin also exposes internal memory charts to help diagnose resident growth in production:
netflow.memory_resident_bytesrss,hwmrss_anon,rss_file,rss_shmem,anon_huge_pages
netflow.memory_resident_mapping_bytes- resident heap bytes
- anonymous non-heap mappings
- raw journal mmap bytes
1mjournal mmap bytes5mjournal mmap bytes1hjournal mmap bytes- GeoIP ASN MMDB resident bytes
- GeoIP geo/country MMDB resident bytes
- other file-backed mappings
- shmem mappings
netflow.memory_allocator_bytesheap_in_use,heap_free,heap_arenammap_in_use,releasable
netflow.memory_accounted_bytes- facet runtime archived/active/contribution/published/path buckets
- materialized tier indexes
- open tier rows
- GeoIP ASN MMDB resident bytes
- GeoIP geo/country MMDB resident bytes
- unaccounted process RSS remainder
netflow.memory_tier_index_bytes- tier row storage
- tier field dictionaries
- tier lookup tables
- tier schema/index metadata
netflow.decoder_scopes- NetFlow v9 parser scopes
- IPFIX parser scopes
- legacy parser scopes
- persisted decoder namespaces
- hydrated namespace-source mappings
These charts are intended for debugging memory explosions under high-cardinality traffic, not for billing or hard enforcement decisions.
journal.tiers configures retention independently for:
rawminute_1(aliases:1m,minute-1,minute1)minute_5(aliases:5m,minute-5,minute5)hour_1(aliases:1h,hour-1,hour1)
If a tier is omitted, it uses the built-in tier default (10GB / 7d). There
are no top-level journal retention knobs; set retention on each tier you want
to tune.
To make a tier time-only, set size_of_journal_files: null.
To make a tier size-only, set duration_of_journal_files: null.
Performance benchmarking
The plugin ships two complementary benchmarks:
cargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release ingest::bench_tests::bench_ingestion_protocol_matrix -- --ignored --nocaptureunpaced full UDP→journal max throughput per protocol, plus decode-only and post-decode phasescargo test -p netflow-plugin --manifest-path src/crates/Cargo.toml --release ingest::resource_bench_tests::bench_resource_envelope_child -- --ignored --nocapturepaced post-decode resource envelope at a configurable rate, controlled via env vars:NETFLOW_RESOURCE_BENCH_PROTOCOL,NETFLOW_RESOURCE_BENCH_PROFILE,NETFLOW_RESOURCE_BENCH_LAYER,NETFLOW_RESOURCE_BENCH_FLOWS_PER_SEC,NETFLOW_RESOURCE_BENCH_WARMUP_SECS,NETFLOW_RESOURCE_BENCH_MEASURE_SECS
The resource-envelope benchmark scope:
- pre-decoded flow records pushed into the ingest pipeline (UDP receive and protocol decode are excluded; measure decode separately with the protocol matrix benchmark)
- full pipeline active: raw journal + 1-minute + 5-minute + 1-hour tier accumulation, real disk-backed journals
- enrichment is NOT loaded (no GeoIP MMDB, no static metadata, no classifiers, no static networks); cardinality fields are pre-populated by the harness
- reports achieved flows/s, CPU% of one core, peak/final RSS, real disk read
and write bytes/s from
/proc/self/io
cpu_percent_of_one_core is the sum of user+system ticks across all threads
of the test process during the measurement window, divided by wall time, as a
percent of one core. 100% means one core's worth of CPU was consumed; values
above 100% are normal for multi-threaded saturation.
Reference measurements:
- CPU:
12th Gen Intel(R) Core(TM) i9-12900K - storage: ext4 on
Seagate FireCuda 530 - methodology: release mode,
5swarmup,15smeasurement window, disk-backed journals, post-decode paced ingest, all-tiers-batched layer
Paced post-decode resource envelope (3A)
Per protocol, per cardinality, at 10 offered rates from 100 to 60 000 flows/s. Cardinality is synthetic: low-cardinality cycles 256 unique records, high- cardinality cycles 4 096 unique records. Real exporter data sits between the two.
Low cardinality, NetFlow v9:
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 80 | 0.3% | 95 KiB/s | 13 MiB |
| 1 000 | 1 000 | 1.3% | 804 KiB/s | 23 MiB |
| 10 000 | 10 000 | 12.6% | 7.7 MiB/s | 75 MiB |
| 30 000 | 30 000 | 35.7% | 22.9 MiB/s | 83 MiB |
| 60 000 | 60 000 | 70.3% | 45.6 MiB/s | 98 MiB |
Low cardinality, IPFIX:
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 61 | 0.1% | 75 KiB/s | 13 MiB |
| 1 000 | 975 | 1.0% | 730 KiB/s | 24 MiB |
| 10 000 | 9 996 | 11.5% | 6.8 MiB/s | 61 MiB |
| 30 000 | 29 988 | 32.9% | 20.4 MiB/s | 83 MiB |
| 60 000 | 59 977 | 64.1% | 40.8 MiB/s | 78 MiB |
Low cardinality, sFlow:
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 99 | 0.2% | 107 KiB/s | 13 MiB |
| 1 000 | 985 | 1.5% | 847 KiB/s | 22 MiB |
| 10 000 | 9 989 | 16.9% | 8.4 MiB/s | 75 MiB |
| 30 000 | 29 967 | 46.2% | 25.2 MiB/s | 84 MiB |
| 60 000 | 59 984 | 87.1% | 50.3 MiB/s | 80 MiB |
High cardinality, NetFlow v9 (saturates around 30 000 flows/s):
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 80 | 0.5% | 409 KiB/s | 25 MiB |
| 1 000 | 1 000 | 4.3% | 2.0 MiB/s | 58 MiB |
| 10 000 | 10 000 | 36.8% | 7.2 MiB/s | 104 MiB |
| 30 000 | 29 331 | 98.0% | 24.9 MiB/s | 119 MiB |
| 60 000 | 26 475 | 98.8% | 30.3 MiB/s | 247 MiB |
High cardinality, IPFIX (saturates around 30-40 000 flows/s):
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 60 | 0.2% | 214 KiB/s | 22 MiB |
| 1 000 | 961 | 3.2% | 2.0 MiB/s | 61 MiB |
| 10 000 | 9 970 | 28.0% | 7.7 MiB/s | 121 MiB |
| 30 000 | 29 985 | 84.8% | 23.0 MiB/s | 121 MiB |
| 60 000 | 28 835 | 98.5% | 36.8 MiB/s | 193 MiB |
High cardinality, sFlow (saturates around 30 000 flows/s):
| offered | achieved | CPU | disk write | RAM peak |
|---|---|---|---|---|
| 100 | 100 | 0.6% | 604 KiB/s | 29 MiB |
| 1 000 | 999 | 4.7% | 3.2 MiB/s | 77 MiB |
| 10 000 | 9 990 | 35.3% | 9.9 MiB/s | 113 MiB |
| 30 000 | 29 257 | 98.3% | 30.9 MiB/s | 129 MiB |
| 60 000 | 30 227 | 98.6% | 29.1 MiB/s | 122 MiB |
Unpaced full UDP→journal protocol matrix (3B)
Single-threaded peak throughput at native fixture cardinality:
| protocol | full ingest (decode + journal) | decode only | post-decode only |
|---|---|---|---|
| NetFlow v9 | 99 000 flows/s | 811 000 flows/s | 116 000 flows/s |
| IPFIX | 107 000 flows/s | 807 000 flows/s | 124 000 flows/s |
| sFlow | 88 000 flows/s | 2 392 000 flows/s | 99 000 flows/s |
Interpretation
- The post-decode ingest hot path is currently single-threaded. CPU pins at ~98-99% of one core at saturation; it does not scale further with more cores.
- Low-cardinality saturation is above 60 000 flows/s for NetFlow v9 and IPFIX, and around 70 000 flows/s for sFlow on this host. The matrix above does not reach those ceilings on purpose; extrapolate from the CPU% column.
- High-cardinality saturation is around 30 000 flows/s post-decode for all three protocols. Above the knee, achieved rate stays at the plateau while offered rate grows.
- Adding decode (~10 µs/flow) on top of post-decode ingest brings the practical full-path ceiling to roughly 22-25 000 flows/s at high cardinality on this host, with the four-tier pipeline running and no enrichment. UDP socket receive is not measured; at these flow rates packet rate (1-5k pps) is well below typical socket limits.
- Disk reads stay near zero because the benchmark isolates the ingest path from query workloads. The journals themselves are indexed and rewrite pages during normal operation; this benchmark does not exercise that overhead at steady state because it only runs for the warmup + measurement window.
- Higher cardinality raises steady memory and per-flow encoding cost, lowering the throughput ceiling.
- These numbers are specific to this host. They do not include GeoIP/MMDB or any other enrichment; loading enrichment adds per-lookup CPU cost on top.
plugins.d protocol
When stdout is not a TTY (normal plugins.d runtime), netflow-plugin emits
PLUGIN_KEEPALIVE periodically to avoid parser inactivity timeouts.