Akvorado on Cisco IOS XE: NetFlow Analytics with ClickHouse

Feed NetFlow v9 from Cisco IOS XE into Akvorado v2026.10.1: SNMPv3 interface enrichment, classifiers, Sankey, top talkers, and the ClickHouse AVX2 crash with its fix. Built in my home lab.

Akvorado on Cisco IOS XE: NetFlow Analytics with ClickHouse

The Grafana and Loki flow setup from earlier in this blog answers "who used the link" well enough for one router. Once you want to slice traffic by interface, network, protocol and exporter in a few clicks, you want a real flow tool. This post feeds NetFlow v9 from two Cisco IOS XE 17.18.2 routers into Akvorado v2026.10.1, an open source flow collector that enriches every flow with SNMP interface data and stores it in ClickHouse. Everything here was built in my home lab, and every screenshot is from that build.

If Flexible NetFlow is new to you, start with Flexible NetFlow on Cisco IOS XE and NetFlow versions compared. The Loki version of this is NetFlow and IPFIX in Grafana. This post is the first in a short series on flow visibility.

Lab diagram: Akvorado on Cisco IOS XE, the flow path. The access switch acc-sw1 (10.0.10.0/24 access LAN) connects to wan-rtr1, which links to wan-rtr2 over the WAN link 10.0.12.0/30. Both routers run a flow monitor on input Et0/1 and Et0/2 and export NetFlow v9 over UDP 2055, unsampled, to the Akvorado inlet at 10.66.20.48. The inlet writes raw packets to Kafka; the outlet at 10.66.20.49 decodes flows, polls the routers over SNMPv3 for interface names, descriptions and speeds, runs classifiers, and writes rows to ClickHouse 26.3. The console shows time series, Sankey and grid views and a top talkers table. Default retention: raw flows 15 days, 1-minute rollups 1 week, 5-minute 3 months, 1-hour 1 year. Callout: ClickHouse 26.8 needs AVX2 (since 26.6); on an AVX-only Xeon, pin 26.3.
Both routers export unsampled NetFlow v9 to the Akvorado inlet. The outlet adds SNMPv3 interface data and writes to ClickHouse, which the console reads.

What Akvorado is, and what it costs

Akvorado is written by Vincent Bernat and released under the AGPL-3.0 licence. There's no paid edition and no feature gate. If you modify it and offer it as a service to others, the AGPL asks you to publish your changes. Running it internally to watch your own network asks nothing of you.

It is several services, not one binary:

  • inlet receives NetFlow v5/v9, IPFIX and sFlow and writes the raw packets to Kafka.
  • outlet decodes them, polls exporters over SNMP for interface names, descriptions and speeds, runs your classification rules and writes rows to ClickHouse.
  • orchestrator hands configuration to the other services and creates the ClickHouse tables.
  • console is the web UI: a home page, and a Visualize page with time series, Sankey and grid views.

The upstream quickstart runs all of this, plus Kafka, ClickHouse, Redis and a Traefik reverse proxy, with Docker Compose. With my traffic loaded, ClickHouse used about 660 MB of RAM and Kafka about 700 MB. The Akvorado services themselves were 13 to 40 MB each. Plan for 2 GB of RAM and a few GB of disk for a small site. Disk grows with flow count, and ClickHouse compresses well.

The setup

Two routers, wan-rtr1 and wan-rtr2, connected by 10.0.12.0/30. Behind wan-rtr1 sits the access switch acc-sw1 on 10.0.10.0/24. Each router has a loopback in 10.255.0.0/24. Flow and SNMP traffic go over a separate management network, 10.66.20.0/24, which lives in a management VRF on the routers. The Akvorado inlet is 10.66.20.48 and the outlet, which does the SNMP polling, is 10.66.20.49.

One honest note before the screenshots: the traffic is synthetic. Python scripts drive the routers and the switch over SSH and run bursts of pings of different sizes between the LAN, the link and the loopbacks. There's real routing protocol traffic too (OSPF and the BGP session between the routers), but no internet traffic, so the AS and country fields stay empty.

Step 1: install Akvorado

mkdir akvorado && cd akvorado
curl -sL https://github.com/akvorado/akvorado/releases/latest/download/docker-compose-quickstart.tar.gz | tar zxf -
docker compose up -d

Before you run it, look at .env. It picks which compose files load. I commented out the IPinfo GeoIP line because nothing in my lab has a public address. Any change you make to ports or images goes in docker/docker-compose-local.yml, which the upstream files leave for you.

Gotcha: ClickHouse dies with exit code 132

My first start failed. The ClickHouse container kept restarting with status 132 and the log was full of this:

clickhouse-1  | /entrypoint.sh: line 42:    23 Illegal instruction     (core dumped) clickhouse extract-from-config ...
akvorado-clickhouse-1 Restarting (132) 2 seconds ago

Exit code 132 is SIGILL, an illegal CPU instruction. Since 26.6, ClickHouse on x86 needs a CPU with AVX2 (x86-64-v3). My server is an older Xeon E5-2670, which has AVX but not AVX2. The release I downloaded asks for ClickHouse 26.8. The fix is in a comment in the upstream docker-compose-local.yml: pin the last release that still runs on older CPUs.

services:
  clickhouse:
    image: clickhouse/clickhouse-server:26.3

Check your CPU first with grep -o -w avx2 /proc/cpuinfo | head -1. If that prints nothing, pin 26.3. After that, every service came up healthy and the console answered on port 8081.

Two other local changes: Traefik's private dashboard port, 8080, clashed with another web app on the same host, so I moved it. And I gave the inlet and outlet fixed addresses on the management network so the routers can export to a stable IP and the SNMP access list can allow the poller.

Step 2: tell Akvorado about your network

Everything you need to touch is in config/outlet.yaml. Three sections matter.

Networks. Akvorado tags each source and destination address with a name, role and site when it falls inside a prefix you list. This is what makes the Sankey diagram readable.

networks:
  networks:
    10.0.10.0/24: {name: access-lan, role: users, site: home-lab}
    10.0.12.0/30: {name: wan-link, role: wan, site: home-lab}
    10.255.0.0/24: {name: router-loopbacks, role: infra, site: home-lab}
    10.66.20.0/24: {name: oob-mgmt, role: mgmt, site: home-lab}

SNMP credentials. The outlet polls each exporter for interface names, descriptions and speeds. The default config uses SNMPv2c with community public. My routers only allow SNMPv3 with authentication and privacy, so I replaced it:

metadata:
  providers:
    - type: snmp
      credentials:
        10.66.20.0/24:
          user-name: zbxmon
          authentication-protocol: SHA
          authentication-passphrase: <AUTH_PASS>
          privacy-protocol: AES
          privacy-passphrase: <PRIV_PASS>

The key is a prefix, so one block covers every exporter on the management network. Lock the file down with chmod 600, since it now holds real passphrases.

Core settings and classifiers.

core:
  default-sampling-rate: 1
  exporter-classifiers:
    - ClassifySiteRegex(Exporter.Name, "^([^.]+)", "$1")
    - ClassifyRegion("home-lab")
    - ClassifyRole("wan-edge")
  interface-classifiers:
    - Interface.Description startsWith "to wan" && ClassifyConnectivity("wan") && ClassifyExternal()
    - Interface.Description startsWith "to acc" && ClassifyConnectivity("access") && ClassifyInternal()
    - ClassifyInternal()

default-sampling-rate is the one people miss. IOS XE doesn't put the sampling rate in its v9 export, and Akvorado rejects flows that arrive without one. I don't sample, so the rate is 1. If you configure a sampler on the router, set the real value here, or every byte count will be off by that factor.

The interface classifiers read the SNMP interface description. My router links are described to wan-rtr2, to acc-sw1 and so on, so a startsWith match is enough. On a real edge router you'd match descriptions like Transit: ISP-A instead. Interface descriptions suddenly become data, which is a good reason to keep them consistent.

The orchestrator watches the config files and restarts itself when they change. The outlet only reads its config at startup, so restart it too: docker compose restart akvorado-outlet. If a change doesn't seem to apply, check the orchestrator logs for a YAML error.

Step 3: configure the routers

Akvorado's docs say to sample on ingress, on every interface, with 10-second active and inactive timeouts, and their IOS XE example uses NetFlow v9. I don't sample, and I also export the interface table. Here's the config I used on both routers.

flow record AKV-REC
 match ipv4 tos
 match ipv4 protocol
 match ipv4 source address
 match ipv4 destination address
 match transport source-port
 match transport destination-port
 match interface input
 collect interface output
 collect transport tcp flags
 collect counter bytes long
 collect counter packets long
 collect timestamp sys-uptime first
 collect timestamp sys-uptime last
!
flow exporter AKV-EXP
 destination 10.66.20.48 vrf <MGMT-VRF>
 source Ethernet0/0
 transport udp 2055
 export-protocol netflow-v9
 template data timeout 60
 option interface-table
 option sampler-table
!
flow monitor AKV-MON
 exporter AKV-EXP
 record AKV-REC
!
interface Ethernet0/1
 ip flow monitor AKV-MON input
interface Ethernet0/2
 ip flow monitor AKV-MON input
!
ip access-list standard ZBX-SNMP
 40 permit 10.66.20.49

Why each piece is there:

  • Input only. If you monitor both directions on every interface, every packet is counted twice. Akvorado gets the output interface from the collect interface output field, so ingress monitoring still tells you where traffic left.
  • vrf on the destination. Without it, the router looks for the collector in the global table and sends nothing, with no error.
  • The ACL line. The outlet polls SNMP from its own address. If your SNMP group has an access list, the poller must be on it. Otherwise flows arrive but interface names stay blank.
  • template data timeout 60. After a collector restart, Akvorado can't decode anything until it sees the template again.

The cache timeout problem

Akvorado's docs also suggest cache timeout active 10 and cache timeout inactive 10 under the monitor. The IOS XE 17.18.2 image I used doesn't have the cache command at all:

wan-rtr1(config)#flow monitor AKV-MON
wan-rtr1(config-flow-monitor)#cache ?
% Unrecognized command

So the defaults apply: 15 seconds inactive, 1800 seconds active. The router only reports a flow when it goes quiet for 15 seconds, or after 30 minutes. Akvorado then counts all of that flow's bytes at the moment it arrives. You'll see the effect in the graphs below as spikes. On a Catalyst 9000, ISR or ASR, set both timeouts as the docs say and the graphs smooth out.

Step 4: check the router before the console

wan-rtr1#show flow exporter AKV-EXP statistics
Flow Exporter AKV-EXP:
  Packet send statistics (last cleared 00:11:41 ago):
    Successfully sent:         30                    (5235 bytes)
  Client send statistics:
    Client: Option options interface-table
      Records added:           10
        - sent:                10
    Client: Flow Monitor AKV-MON
      Records added:           41
        - sent:                41

The interface table went out, and every flow record added was also sent. The cache shows what's being tracked right now:

wan-rtr1#show flow monitor AKV-MON cache format table
  Flows added:                                  47
  Flows aged:                                   41
    - Active timeout      (  1800 secs)          0
    - Inactive timeout    (    15 secs)         41

IPV4 SRC ADDR    IPV4 DST ADDR    TRNS SRC PORT  TRNS DST PORT  INTF INPUT  IP PROT  intf output   bytes long
10.0.12.2        10.0.12.1                59034            179  Et0/1             6  Null                7147
10.0.12.2        224.0.0.5                    0              0  Et0/1            89  Null                7200
10.0.10.2        10.255.0.2                   0           2048  Et0/2             1  Et0/1          121312000
10.255.0.2       10.0.10.2                    0              0  Et0/1             1  Et0/2          119255200

Every flow aged out on the inactive timer, none on the active one. Note the BGP session (port 179) and OSPF hellos (protocol 89 to 224.0.0.5) next to the test pings. Output Null means the router itself was the destination.

On the Akvorado side, the quickest check is ClickHouse itself:

docker compose exec clickhouse clickhouse-client -q "
  SELECT ExporterName, InIfName, InIfDescription, count(), sum(Bytes)
  FROM flows WHERE TimeReceived > now() - 600
  GROUP BY 1,2,3 ORDER BY 5 DESC"

wan-rtr2.lab   Et0/1   to wan-rtr1   111   576173453
wan-rtr1.lab   Et0/2   to acc-sw1    106   574673853
wan-rtr1.lab   Et0/1   to wan-rtr2   110   568644306

If InIfName and InIfDescription come back filled in, SNMP polling works. If they're empty, check the SNMP credentials and the access list on the router first.

The console

Traffic over time, by exporter and interface

Open Visualize, pick the ExporterName and InIfName dimensions, and you get a stacked graph of traffic per ingress interface across both routers.

Akvorado Visualize page showing stacked traffic over 20 minutes for wan-rtr1 Et0/1, wan-rtr2 Et0/1 and wan-rtr1 Et0/2, with a table of min, max, average, 95th percentile and total
Ingress traffic per exporter and interface over 20 minutes. Click to enlarge.

The tall spikes before 21:35 are from my first traffic run, where pings ran for up to 45 seconds with short gaps. With a 15-second inactive timer, those turned into long flows that were reported in one go. After I switched to short bursts with gaps longer than 15 seconds, the graph looks much more like real traffic. That's the cache timeout problem from above, and it's why you should set those timeouts on hardware that supports them.

SNMP enrichment and interface classification

Add InIfDescription, InIfSpeed, InIfConnectivity and InIfBoundary as dimensions and you can see the outlet's work.

Akvorado table with exporter name, interface name, SNMP interface description, speed, connectivity and boundary for each ingress interface
Interface names, descriptions and speeds come from SNMPv3. Connectivity and boundary come from the classifiers. Click to enlarge.

The router only sends an interface index in each flow. Et0/1, to wan-rtr2 and the speed came from SNMPv3 polling. wan/access and external/internal came from my two classifier lines. The speed column reads 10 (Mbps) because that's what these interfaces report over SNMP. It's also why the peaks look higher than the link speed: a whole burst gets counted in a single interval.

Sankey: where traffic comes from and goes

Switch the graph type to Sankey and pick SrcNetName, InIfConnectivity and DstNetName. Those network names are the ones from the networks block.

Akvorado Sankey diagram from source network through ingress connectivity to destination network, showing access-lan, router-loopbacks and wan-link flows
Source network to ingress connectivity to destination network. Click to enlarge.

This is the view I'd put in front of someone who doesn't read flow tables. Traffic from access-lan enters on the access side and heads mostly to router-loopbacks. The return traffic comes in from the WAN side. On a real edge you'd use InIfProvider or DstAS here and see which upstream carries which customers.

Top talkers

Set the dimensions to SrcAddr and DstAddr. The table under the graph is your top-talkers list, ranked by average rate, with 95th percentile and total bytes.

Akvorado top talkers by source and destination address over 20 minutes, led by 10.0.10.2 to 10.255.0.2
Top conversations by source and destination address. Click to enlarge.

The top pair, 10.0.10.2 to 10.255.0.2, is the switch pinging the far router's loopback: 930 MB in 20 minutes. To drill down, put SrcAddr = 10.0.10.2 in the filter box and change the dimensions to InIfName and OutIfName.

Protocols and ports

Akvorado line graph by protocol and destination port: ICMP 0 and ICMP 2048 dominate, with small TCP flows to SSH, NETCONF and high ports
Traffic by protocol and destination port. Click to enlarge.

ICMP shows destination ports 2048 and 0. NetFlow puts ICMP type and code in the destination port field as type × 256 + code, so 2048 is echo request (type 8) and 0 is echo reply. The small TCP flows come from a separate script that opens connections to ports 22, 23, 80, 443 and 830 on a router. The high destination ports are the replies going back. That script comes in handy later in this series.

What it doesn't do (in this lab)

  • No AS paths or countries. That needs public addresses, a GeoIP database and BMP or BGP feeding the outlet. Akvorado supports all of that, but there's nothing in a lab to show.
  • Coarse timing on this image. Without cache timeout, long flows only report every 30 minutes. Hardware platforms don't have this problem.
  • No alerting. Akvorado is for looking at traffic. For alerts, query ClickHouse from Grafana or send its metrics to Prometheus.
  • Retention. By default, raw flows are kept for 15 days, then 1-minute rollups for a week, 5-minute for three months and 1-hour for a year. A query over last month is answered from the 5-minute table, not individual flows. You can change this with resolutions in akvorado.yaml.

Troubleshooting checklist

  • ClickHouse restarting with 132: CPU lacks AVX2. Pin clickhouse-server:26.3.
  • No flows in the console: run show flow exporter ... statistics on the router. If "sent" is climbing, check the destination IP, the vrf keyword and that UDP 2055 reaches the inlet.
  • Flows but blank interface names: SNMP. Check the credentials in outlet.yaml and that the outlet's IP is on the router's SNMP ACL.
  • Flows dropped with no error: no sampling rate. Set default-sampling-rate.
  • Graph spikes instead of smooth lines: active timeout too long. Set it to 60 seconds or less where your platform allows.

Next in this series: using these same flows to answer "who is eating the WAN" during an actual incident, step by step.

Now live: Troubleshooting with NetFlow: who is eating the WAN? uses these flows to track down a bandwidth hog.

Read next