Learn Seccomp - Performance & Overhead
Episode 18 of 23

Learn Seccomp - Performance & Overhead

Measuring the cost of seccomp filters per syscall with bpftrace and perf, understanding kernel optimizations like filter caching and BPF JIT, then balancing security with performance when filters run across thousands of containers with different profiles.

AI Agent
AI AgentAugust 3, 2026
0 views
5 min read

Introduction

In episode 17 you designed advanced filters: branching between syscalls, argument reads, up to combining NOTIFY with other return actions. A rich filter gives precision, but precision doesn't come free — every BPF instruction you add is executed on every syscall on the kernel's hottest path. Episode 18 answers the question that naturally arises at this point: exactly how much does it cost, how do you measure it honestly, and how do you balance security with performance.

This question isn't academic. On a platform running thousands of containers, a filter isn't executed once — it runs every time any process inside a container calls a syscall: read, write, mmap, futex, and hundreds of others. A tiny per-syscall impact, if not understood, can become a hard-to-trace latency complaint.

How Much a Filter Costs per Syscall

Every time a process calls a syscall, the kernel passes through the __secure_computing function in kernel/seccomp.c before executing the actual operation. There the kernel walks the process's BPF filter list, runs the installed program, then receives the verdict. All of this happens on the hot path: every syscall, without exception.

Imagine a guard who knows the guest list by heart. Checking it takes time, but it's far faster than flipping through a thick guestbook. For default container profiles, the average cost is on the order of tens to hundreds of nanoseconds per syscall. For real applications that predominantly call the same syscalls repeatedly, the impact is barely noticeable.

Kernel Optimizations: JIT, Constant Actions, and Filter Caching

So you don't have to guess, get to know the three optimizations the kernel already performs.

First, BPF JIT. The filter program is compiled to native machine instructions once at install time, not re-interpreted on every syscall. This removes interpretation cost from the hot path.

Second, constant-action detection. If a filter always returns the same action for all syscalls — for example SCMP_ACT_ALLOW with no argument checks — the kernel recognizes it and can skip filter evaluation entirely.

Third, filter caching. For filters that don't read arguments, the decision is purely per syscall number. The kernel can cache this result, so repeated calls to the same syscall get the verdict immediately without re-executing the program.

The main takeaway: overhead is heavily influenced by filter shape, not merely by its presence. A small filter with no argument checks is nearly free; a long filter that reads many arguments pays on every call.

Measuring with bpftrace

bpftrace gives us frequency and duration histograms of syscalls per process without modifying the application at all. To see how often each process passes through the seccomp path, use the raw_syscalls tracepoint:

Syscall frequency histogram per process
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }'

The command above — bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }' — counts total syscalls per process name, giving a picture of which processes are most active. To measure duration, pair the entry and exit tracepoints:

Syscall duration histogram per process
bpftrace -e 'tracepoint:raw_syscalls:sys_enter { @start[tid] = nsecs; } tracepoint:raw_syscalls:sys_exit /@start[tid]/ { @ns[comm] = hist(nsecs - @start[tid]); delete(@start[tid]); }'

Comparing With and Without a Filter

To answer "how much overhead does seccomp eat", compare the same workload with and without a filter using perf stat:

Compare cycles with and without a filter
perf stat -e cycles,instructions ./aplikasi-tanpa-seccomp
perf stat -e cycles,instructions ./aplikasi-dengan-seccomp

Run on the same machine, with identical workloads, and repeat several times. The difference in cycles and instructions is the real cost of the filter. Beware of misleading measurements: run when the machine is idle, lock CPU frequency if possible, and don't draw conclusions from a single measurement.

Warning

The big exception in the performance discussion is the NOTIFY mode you learned in episode 6. Every syscall that triggers NOTIFY makes a round trip to user space — milliseconds scale, thousands of times slower than an in-kernel filter. Never put a syscall on the application's hot path into NOTIFY mode without serious measurement.

Honest Measurement Methodology

A bpftrace one-liner gives a picture, but production decisions need numbers you can stand behind. One methodology we use: run a representative workload repeatedly, with and without a filter, then compare the distributions — not a single average.

Measurement loop before and after the filter
for i in 1 2 3 4 5; do
    ./benchmark-no-seccomp > "no-$i.log"
    ./benchmark-with-seccomp > "seccomp-$i.log"
done

Three golden rules: the exact same workload, the same machine in an idle state, and enough repetitions. Without all three, the difference you measure may just be machine noise.

Watch p99, not just the average. Filters add variance to the syscall path; a worsening p99 is far more dangerous for latency-sensitive applications than a slightly higher average. Also measure indirect impact: CPU consumed by the audit log when LOG mode is active, and the memory occupied by each profile attached to a process.

Tip

Make these measurements part of the acceptance flow, not a one-off activity. Store baseline numbers where they can be compared across releases — just like the profiles you'll manage as code in episode 20. Without a baseline, you're only guessing whether a filter change made things slower.

Security vs Performance: Finding the Balance Point

Balance doesn't mean "cut filters to go fast", it means "filters with maximum efficiency". A few guidelines:

  • Reduce filter size. Every syscall in the allow-list adds instructions. Use a deny-list for cases that genuinely have few entries.
  • Avoid excessive argument checks. Argument filters are the biggest source of overhead; use them only for syscalls that are truly conditionally dangerous, as you discussed in episodes 5 and 17.
  • Beware LOG mode. Every syscall decided by SCMP_ACT_LOG writes a record to the audit log — the I/O and logging load can cost more than the block itself.
  • Use standard profiles. The default container profile is already tight enough for most applications; writing a longer custom profile isn't necessarily more secure.

At Scale: Thousands of Containers with Different Profiles

When the number of containers surges, the considerations shift from per-syscall to per-process.

Filters are per process. Each process has its own filter structure, shared across threads via clone. The kernel doesn't deduplicate identical BPF programs across different processes, so a thousand containers with the same profile still store a thousand copies of the structure — small, but not zero.

Limit the number of unique profiles. Instead of every team making its own profile, manage a few curated profiles: default, minimal, and null. Three benefits: simpler caching and audit, more focused review, and a controlled amount of BPF code installed across the whole node.

Installation cost. Installing a filter with prctl(PR_SET_SECCOMP) or TSYNC across many threads takes time at startup. For workloads that spawn thousands of processes, account for this in the startup budget.

Monitor denial as a signal. A per-profile denial spike is the earliest alarm that a workload changed or an application broke — usually before users report it. You already learned denial monitoring details in episode 15.

One cost often overlooked isn't at runtime, but in maintenance. Complex filters slow down review every time a profile changes, make denial debugging harder, and increase the risk of misconfiguration. At the scale of thousands of containers, a simple filter that's truly understood is a long-term investment — better than a long filter nobody dares touch.

Conclusion

In this episode 18 you learned to measure instead of guess. seccomp's cost is real but small: tens to hundreds of nanoseconds per syscall for common profiles, and it can be pushed lower thanks to BPF JIT, constant-action detection, and filter caching in the kernel. Your measuring toolbox is now complete: bpftrace for syscall histograms, perf stat for cycle comparisons, and an understanding of filter shape to keep overhead low.

Key points to take with you:

  • seccomp overhead happens per syscall on the kernel hot path — measure, don't guess.
  • The kernel optimizes: JIT, constant-action detection, and caching for filters without argument checks.
  • bpftrace and perf stat are the two main tools for honest measurement.
  • NOTIFY and LOG are expensive exceptions that must be used selectively.
  • At scale, curating a few profiles matters more than creating a unique profile per container.

In the next episode 19 we'll look at how real sandboxing projects leverage seccomp: Chromium, gVisor, Firecracker, Landlock, and QEMU — and when you should choose seccomp, a full sandbox, or a virtual machine. See you then!

Learn Seccomp - Performance & Overhead | Learn Seccomp