On a modern dedicated server, the storage drive is often no longer
the slowest part of an I/O request. An NVMe SSD can finish a 4K read in tens of
microseconds, so the software path around it (system calls, context switches, interrupt
handling) starts to dominate. io_uring is the Linux
kernel's answer to that problem. It lets an application submit and collect large batches
of asynchronous I/O through shared ring buffers instead of paying for one system call
per operation.
This tutorial shows you how to verify io_uring on Ubuntu 24.04, install liburing and fio,
run a repeatable benchmark, compare io_uring against
libaio and synchronous I/O, and read the results
correctly on your own bare metal server. It suits database hosts, virtualization nodes,
storage servers, log pipelines, and busy web servers.
Benchmark note: Results depend on CPU, RAM, kernel version, storage device, filesystem, queue depth, block size, and background load. Treat every number you record as a baseline for your dedicated server, not a universal claim.
What io_uring does: batches asynchronous I/O through a submission queue (SQ) and a completion queue (CQ), cutting per-operation system-call overhead.
What you need: Ubuntu 24.04 LTS, root or sudo access, liburing-dev, fio, sysstat, and ideally an NVMe SSD.
Core command: fio --ioengine=io_uring --direct=1 --iodepth=32
against a dedicated test file, never a production dataset.
How to judge results: compare IOPS, latency percentiles, and CPU cost together, across several queue depths and repeated runs.
Key caveat: io_uring is not automatically faster than libaio for every workload. Your own benchmark is the only reliable answer.
io_uring is an asynchronous I/O
interface built into the Linux kernel. Traditional calls such as read(), write(),
pread(), and pwrite() cost one system call per operation. With
io_uring, the application places requests in a
submission queue (SQ) and reads results from a completion queue (CQ). Both queues live
in memory shared between the application and the kernel.
Key concepts you will see throughout this guide:
Submission queue (SQ): holds I/O requests waiting to be processed.
Completion queue (CQ): reports finished operations back to the application.
Queue depth: how many operations can be in flight at once.
Asynchronous I/O: operations progress without the application blocking on each one.
Batch submission: many requests are submitted with a single system call.
liburing: the userspace helper library that wraps the raw io_uring system calls.
Because of this queue-based, batched design, io_uring is most relevant to high-concurrency
workloads: databases, storage services, web servers, virtualization platforms, and any
application that performs large volumes of small, frequent reads and writes.
A dedicated server gives you unshared access to physical CPU cores, RAM, storage, and network. That makes I/O efficiency easier to measure and more valuable to improve than on shared or heavily virtualized infrastructure, where noisy neighbors add variance.
On an NVMe dedicated server, the drive can often complete an operation
faster than the software path around it. At that point, system-call overhead becomes the
practical bottleneck. io_uring can help by offering:
Lower per-operation system-call overhead
Efficient batching of large numbers of I/O requests
Better support for high I/O concurrency
More effective use of fast NVMe SSDs
Reduced CPU cost for I/O-heavy workloads
One interface for asynchronous file and network I/O
The benefit is workload-dependent. Moving an application to io_uring does not speed up every I/O pattern, which is
why benchmarking your own high-performance dedicated server matters more than trusting
generic numbers. If you are still choosing hardware, see our high-performance dedicated
servers and our NVMe dedicated server options.
Before you benchmark io_uring on your dedicated server, prepare the following:
Ubuntu 24.04 LTS
A kernel with io_uring support (the Ubuntu 24.04 default kernel qualifies)
Root or sudo access
GCC or another supported C compiler
Git and Make
liburing-dev (liburing
development files)
fio for storage benchmarking
An NVMe SSD, if you are evaluating NVMe-class performance
Enough free disk space for test files
A way to monitor CPU, memory, disk, and I/O activity
Run tests on an otherwise idle server. Background databases, backups, and migrations distort every result. If you are provisioning a fresh Linux dedicated server, do this before it enters production. That is the cleanest time to capture a baseline.
Confirm which kernel is actually running:
uname -r
uname -a
Ubuntu 24.04 ships with a modern kernel that includes io_uring, but always verify the running kernel on the specific server rather than assuming the distribution default. Provider images, custom kernels, and pinned versions can differ.
Inspect the kernel build configuration:
grep -i IO_URING /boot/config-$(uname -r)
You should see CONFIG_IO_URING=y. If the
flag is missing, check that you are reading the config file for the kernel that is
currently active.
The most reliable check is running a real io_uring program, and fio does exactly that in the next sections. Two quick checks come first.
Check whether the kernel allows io_uring for your users. Recent kernels expose a sysctl that can restrict it:
sysctl kernel.io_uring_disabled
| Value | Meaning |
|---|---|
| 0 | io_uring is available to all processes |
| 1 | io_uring is restricted to privileged processes or a specific group |
| 2 | io_uring is disabled for everyone |
If the value is 1 or 2 and your benchmark fails, this setting is the first thing to review, along with your organization's security policy.
Install the liburing development package and confirm its version:
sudo apt update
sudo apt install liburing-dev
pkg-config --modversion liburing
dpkg -L liburing-dev
liburing is a userspace library. Having
it installed does not guarantee that every io_uring feature exposed by newer kernels is
available on your running kernel.
sudo apt update
sudo apt install build-essential git pkg-config liburing-dev fio sysstat nvme-cli
| Package | Purpose |
|---|---|
| build-essential | Compiler and build utilities |
| git | Source-code retrieval |
| pkg-config | Compiler and library configuration |
| liburing-dev | io_uring development headers and library |
| fio | Flexible I/O benchmark tool |
| sysstat | Monitoring tools such as iostat, mpstat, and sar |
| nvme-cli | NVMe device inspection |
Verify the installs:
gcc --version
git --version
fio --version
iostat -V
fio is the most practical Linux
storage benchmark for this job because it ships with a native io_uring I/O engine. Check
that your build includes it:
fio --enghelp | grep -i io_uring
If io_uring appears in the output, you
can benchmark directly with fio. You do not need to compile a custom tool.
A random-read test with a fixed block size and queue depth gives you a clean baseline. Replace the file path with a safe test location. Never point a benchmark at a production disk or dataset.
fio --name=uring-randread \
--filename=/var/tmp/uring-testfile \
--size=4G \
--rw=randread \
--bs=4k \
--ioengine=io_uring \
--iodepth=32 \
--numjobs=1 \
--direct=1 \
--ramp_time=10 \
--runtime=60 \
--time_based \
--lat_percentiles=1 \
--group_reporting
What the key options do:
--ioengine=io_uring selects the
io_uring engine.
--direct=1 bypasses the page
cache so you measure the device, not RAM.
--iodepth=32 keeps up to 32
operations in flight.
--ramp_time=10 discards the first
10 seconds so warm-up does not skew results.
--lat_percentiles=1 reports
percentile latency, not just the average.
Record these metrics on every run: IOPS, bandwidth, average latency, percentile latency (p99 and p99.9), CPU utilization, queue depth, and the read/write mix.
Keep these identical between runs: test-file size, block size, access pattern, queue depth, job count, runtime, direct I/O setting, storage device, filesystem, and overall server state. One run is not a baseline. Repeat the test at least three times under consistent conditions before drawing conclusions.
fio lets you run the same workload through different I/O engines on the same dedicated server.
libaio (asynchronous, same queue depth):
fio --name=libaio-randread \
--filename=/var/tmp/uring-testfile \
--size=4G \
--rw=randread \
--bs=4k \
--ioengine=libaio \
--iodepth=32 \
--numjobs=1 \
--direct=1 \
--ramp_time=10 \
--runtime=60 \
--time_based \
--lat_percentiles=1 \
--group_reporting
Synchronous I/O (one operation at a time):
fio --name=sync-randread \
--filename=/var/tmp/uring-testfile \
--size=4G \
--rw=randread \
--bs=4k \
--ioengine=psync \
--numjobs=1 \
--direct=1 \
--ramp_time=10 \
--runtime=60 \
--time_based \
--lat_percentiles=1 \
--group_reporting
Synchronous engines cannot use a queue depth above 1, so this run shows what
a blocking application experiences. When you want a fair synchronous comparison against
a deeper queue, raise --numjobs so total concurrency
matches.
Only compare runs that share the same configuration. If the queue depth, file size, or filesystem state changes between runs, you cannot attribute the difference to the I/O engine.
Use this table to build a repeatable baseline for your server:
| Test | I/O Engine | Block Size | Queue Depth | Jobs | IOPS | Bandwidth | Avg Latency | p99 Latency | CPU Usage |
|---|---|---|---|---|---|---|---|---|---|
| io_uring | io_uring | 4K | 32 | 1 | record | record | record | record | record |
| libaio | libaio | 4K | 32 | 1 | record | record | record | record | record |
| sync | psync | 4K | 1 | 1 | record | record | record | record | record |
Reading the results: if io_uring shows
equal IOPS but lower CPU usage, it is doing the same work more cheaply, and that
headroom matters when the CPU is shared with your application. If io_uring and libaio are nearly identical, your workload is probably
limited by the drive or the filesystem rather than by system-call overhead.
On an NVMe dedicated server, parallelism matters far more than on spinning disks or SATA SSDs, because NVMe devices are designed to handle many concurrent operations across multiple hardware queues.
Identify the NVMe device:
lsblk -o NAME,MODEL,SIZE,TYPE,MOUNTPOINT
sudo nvme list
Run a filesystem-level test against a dedicated test directory, not a production device:
sudo mkdir -p /var/tmp/io-test
sudo fio --name=nvme-uring \
--directory=/var/tmp/io-test \
--size=4G \
--rw=randread \
--bs=4k \
--ioengine=io_uring \
--iodepth=64 \
--numjobs=2 \
--direct=1 \
--ramp_time=10 \
--runtime=60 \
--time_based \
--lat_percentiles=1 \
--group_reporting
The best queue depth depends on the SSD and workload, so test a range instead of assuming higher is better. This loop runs the same test at each depth:
for qd in 1 4 16 32 64 128; do
echo "=== iodepth=$qd ==="
fio --name=qd-$qd \
--filename=/var/tmp/uring-testfile \
--size=4G --rw=randread --bs=4k \
--ioengine=io_uring --iodepth=$qd \
--numjobs=1 --direct=1 \
--ramp_time=10 --runtime=30 --time_based \
--lat_percentiles=1 --group_reporting
done
Watch how latency moves as concurrency rises. The point where latency climbs faster than IOPS is usually your practical ceiling. Past that point, extra queue depth adds waiting time without adding throughput.
Once you have a read baseline, repeat the sweep for mixed workloads (for
example --rw=randrw --rwmixread=70) if that matches
your application. Run write tests only against test files, never against a raw device
that holds data.
Benchmark output alone does not explain what happens inside the server. Run monitoring in a second terminal during every fio test.
iostat -xz 1
Key fields to read:
%util: device utilization
r/s, w/s: read and write operations per second
rkB/s, wkB/s: read and write throughput
r_await, w_await: average completion time per operation
aqu-sz: average queue size
Field names vary slightly between sysstat versions, so check your iostat header line.
For CPU behavior:
mpstat -P ALL 1
top
htop
If raising queue depth increases CPU usage without improving IOPS, you have likely hit a different bottleneck: the CPU, the filesystem, or the application itself rather than the storage device.
Establish a clean baseline before changing anything, then adjust one variable at a time and re-run the same test.
CPU and power management
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
Frequency scaling can make results inconsistent between runs. Record the governor you tested with.
NUMA topology
lscpu
numactl --hardware
On multi-socket dedicated servers, the locality between CPU cores, memory, and the NVMe drive's PCIe root can affect sustained high-throughput I/O.
Filesystem
Filesystem type and mount options can shift results. Keep them fixed when comparing I/O engines.
Queue depth and job count
Test several values rather than defaulting to a large one. Very deep queues can raise tail latency with no throughput gain.
CPU affinity
Pinning fio or your application to specific cores is useful for advanced tuning. Apply it only after you have confirmed a measured bottleneck, not preemptively.
Kernel and driver versions
Keep the kernel and storage drivers current according to your production policy. Document the exact kernel version for every benchmark, because io_uring capabilities evolve across kernel releases.
The benchmark will not start: Run the three quick
checks: uname -r, fio --enghelp | grep -i io_uring,
and sysctl kernel.io_uring_disabled
along with pkg-config --modversion liburing.
Low IOPS: Causes include low queue depth, slow storage, CPU limits, filesystem overhead, misconfigured fio parameters, thermal or power throttling, background load, and NUMA placement. Do not blame io_uring until you have compared the complete test configuration.
High latency: Check iostat -xz 1 first, then CPU
usage, device utilization, queue depth, drive temperature, and
competing workloads.
Inconsistent results between runs: Usually caused by caching, filesystem state, background processes, CPU frequency scaling, SSD garbage collection, or thermal throttling. Repeat tests and document the environment each time.
"Invalid argument" with --direct=1: Some filesystems (for example tmpfs) do not support direct I/O. Move the test file to a disk-backed filesystem.
Permission errors: Confirm the test directory is writable by the account running fio, and understand the implications before running any test as root.
A benchmark environment is not automatically representative of production. Before deploying an io_uring-based application to a live dedicated server, verify:
Kernel compatibility
Application-level io_uring support
liburing compatibility where applicable
Storage and filesystem behavior under real load
Security requirements. io_uring exposes a large kernel interface, and
some organizations restrict it. Review kernel.io_uring_disabled and your
security policy before enabling it broadly.
Monitoring and logging coverage
Error-handling paths
Backup and recovery procedures
Application-specific latency requirements
Test the full application workload, not just a synthetic fio run. A database, a web server, a virtualization host, and a log-processing pipeline each stress io_uring differently. For latency-sensitive workloads, server location also matters. Browse our dedicated server locations to place hardware near your users. For storage-heavy workloads, review our storage servers, and for compute-heavy pipelines that also depend on fast I/O, see our GPU servers.
Ubuntu 24.04 updated per your maintenance policy
Active kernel version documented
io_uring support verified with a real workload, not just config flags
kernel.io_uring_disabled value checked
liburing development packages installed where needed
fio confirmed to include the io_uring engine
Storage device identified and documented
Test file stored on the intended device
Production data protected from destructive tests
Block size, queue depth, job count, and runtime documented
Direct I/O setting documented
CPU utilization monitored throughout
IOPS and bandwidth recorded
Average and percentile latency recorded
Multiple runs completed
io_uring compared under identical workload conditions
Results interpreted against the actual target application
io_uring gives Linux a modern, batch-oriented asynchronous I/O path, and it pays off most on a dedicated server that processes large volumes of concurrent storage or network operations. On Ubuntu 24.04, you can verify kernel support, install liburing and fio, run a controlled io_uring benchmark, compare it against libaio and synchronous I/O, and monitor CPU and disk behavior with iostat and mpstat.
The best benchmark is not the one with the highest raw IOPS number. It is the one that connects IOPS, throughput, latency, CPU cost, and queue depth to the workload you actually run in production. For anyone managing a high-performance dedicated server, that repeatable process shows whether io_uring, your NVMe storage, and your kernel configuration are being used efficiently, or whether overhead is still on the table.
Ready to put these results to work on dedicated hardware? Explore COLO BIRD dedicated servers with NVMe storage, or browse more guides in our Linux server tutorials.
It works on any server running a Linux kernel with
io_uring support and where kernel.io_uring_disabled permits it. Ubuntu
24.04 includes support by default. Actual performance gains depend on the CPU,
storage device, filesystem, and application.
Not automatically. io_uring can reduce system-call overhead for suitable high-concurrency workloads, but the only reliable way to know is to benchmark both engines under identical conditions, as shown in Section 9.
No. However, NVMe storage is where the advantages tend to show most clearly, because NVMe devices expose enough parallelism for software overhead to become the limiting factor.
There is no single best value. Test a range such as 1, 4, 16, 32, 64, and 128, then choose the point where latency starts rising faster than IOPS.
Not against production disks or live datasets. Use a dedicated test file or directory, and run benchmarks on an idle server or before the machine goes into service.