ALBANIA

ARGENTINA

AUSTRALIA

AUSTRIA

AZERBAIJAN

BANGLADESH

BELGIUM

BOSNIA AND HERZEGOVINA

BRAZIL

BULGARIA

CANADA

CHILE

CHINA

COLOMBIA

COSTA RICA

CROATIA

CYPRUS

CZECH REPUBLIC

DENMARK

ECUADOR

EGYPT

EL SALVADOR

ESTONIA

FINLAND

FRANCE

GEORGIA

GERMANY

GREECE

GUATEMALA

HUNGARY

ICELAND

INDIA

INDONESIA

IRELAND

ISRAEL

ITALY

JAPAN

KAZAKHSTAN

KENYA

KOSOVO

LATVIA

LIBYA

LITHUANIA

LUXEMBOURG

MALAYSIA

MALTA

MEXICO

MOLDOVA

MONTENEGRO

MOROCCO

NETHERLANDS

NEW ZEALAND

NIGERIA

NORWAY

PAKISTAN

PANAMA

PARAGUAY

PERU

PHILIPPINES

POLAND

PORTUGAL

QATAR

ROMANIA

RUSSIA

SAUDI ARABIA

SERBIA

SINGAPORE

SLOVAKIA

SLOVENIA

SOUTH AFRICA

SOUTH KOREA

SPAIN

SWEDEN

SWITZERLAND

TAIWAN

THAILAND

TUNISIA

TURKEY

UKRAINE

UNITED ARAB EMIRATES

UNITED KINGDOM

URUGUAY

USA

UZBEKISTAN

VIETNAM

How to Install and Benchmark io_uring on Ubuntu 24.04 for a High-Performance Dedicated Server

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.

Quick Summary

  • 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.

1. What Is io_uring?

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.

2. Why io_uring Matters on a Dedicated Server

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.

3. Requirements

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.

4. Check Your Linux Kernel Version

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.

5. Check io_uring Support

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.

6. Install the Required Development and Benchmarking Tools

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

7. Confirm fio includes the io_uring Engine

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.

8. Run an io_uring Benchmark

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.

9. Compare io_uring with libaio and Synchronous I/O

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.

10. Test NVMe Storage at Different Queue Depths

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.

11. Monitor CPU Utilization and IOPS

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.

12. Tune the Dedicated Server

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.

13. Common io_uring Issues and Fixes

  • 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.

14. Production Considerations

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.

15. Final Performance Checklist

  • 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

Conclusion

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.

Frequently Asked Questions

Does io_uring work on any dedicated server?

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.

Is io_uring faster than libaio?

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.

Do I need NVMe storage to benefit from io_uring?

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.

Which fio queue depth should I use on a dedicated server?

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.

Is it safe to run fio on a production server?

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.

```eof The io_uring tutorial has been fully formatted into your exact HTML layout. The tables were built with clean HTML (``), the multi-line `fio` commands have been correctly separated using `
` and ` ` spacing, and all checkboxes, code UI boxes, and FAQs perfectly mimic the original layout you shared! Let me know if you need any adjustments to spacing or tags.