Category: Linux

Approaches to Server Security: Stop Thinking Like It’s 2010

Server Security  /  March 2026

The patterns showing up in server logs over recent months suggest that the attack surface has shifted in some fairly predictable ways. A few straightforward measures appear to address the bulk of it.

The Pattern in the Logs: Digital Ocean

Anyone running a public-facing server and watching their /var/log/auth.log or fail2ban output will likely notice something consistent: a notable proportion of brute force and port scanning activity appears to originate from Digital Ocean IP ranges.

This is not particularly surprising. A low-cost VPS can be provisioned in seconds, carries a clean IP not yet on most blocklists, and can be destroyed without a trace once a campaign is complete. It would appear this has become a fairly common setup for automated credential testing.

This is not a criticism of Digital Ocean specifically. The same pattern appears across AWS, Vultr, Linode and others. It is simply where the activity seems most concentrated at present, based on log observation.

Once you can identify where the traffic is coming from, blocking it at the network level before it reaches your services is relatively straightforward.

Watching the Logs and Blocking at Range Level

Blocking individual IPs as they appear is largely ineffective since the same underlying infrastructure will simply rotate addresses. Watching for patterns across a few days and then blocking the entire subnet tends to be considerably more efficient.

Step 1: Extract the Top Attacking IPs

bash
# Top attacking IPs from auth log
grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -20

Run this over several days. The same /16 or /24 ranges will tend to reappear. That is the signal to act on.

Step 2: Find the Full CIDR Range

bash
whois 167.99.1.1 | grep -i "CIDR\|NetRange\|inetnum"

Step 3: Block the Entire Range

Rather than managing individual IPs, the script below blocks all known Digital Ocean IPv4 ranges in a single pass. Save it as block-digitalocean.sh and run as root. It skips ranges already blocked, detects your OS, and persists the rules across reboots on Debian, Ubuntu, AlmaLinux, and RHEL.

bash
sudo chmod +x block-digitalocean.sh
sudo ./block-digitalocean.sh

The Script: block-digitalocean.sh

bash: block-digitalocean.sh
#!/bin/bash
#
# Block Digital Ocean IP Ranges
# Usage: sudo ./block-digitalocean.sh
#

set -e

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

if [[ $EUID -ne 0 ]]; then
   echo -e "${RED}Error: This script must be run as root${NC}"
   exit 1
fi

DO_RANGES=(
    "5.101.0.0/16"    "24.144.0.0/16"   "24.199.0.0/16"
    "37.139.0.0/16"   "45.55.0.0/16"    "46.101.0.0/16"
    "64.23.0.0/16"    "64.225.0.0/16"   "64.226.0.0/16"
    "64.227.0.0/16"   "67.205.0.0/16"   "67.207.0.0/16"
    "68.183.0.0/16"   "69.55.0.0/16"    "80.240.0.0/16"
    "82.196.0.0/16"   "95.85.0.0/16"    "103.253.0.0/16"
    "104.131.0.0/16"  "104.236.0.0/16"  "104.248.0.0/16"
    "107.170.0.0/16"  "128.199.0.0/16"  "129.212.0.0/16"
    "134.122.0.0/16"  "134.199.0.0/16"  "134.209.0.0/16"
    "137.184.0.0/16"  "138.68.0.0/16"   "138.197.0.0/16"
    "139.59.0.0/16"   "141.0.0.0/16"    "142.93.0.0/16"
    "143.110.0.0/16"  "143.198.0.0/16"  "143.244.0.0/16"
    "144.126.0.0/16"  "146.185.0.0/16"  "146.190.0.0/16"
    "147.182.0.0/16"  "152.42.0.0/16"   "157.230.0.0/16"
    "157.245.0.0/16"  "159.65.0.0/16"   "159.89.0.0/16"
    "159.203.0.0/16"  "159.223.0.0/16"  "161.35.0.0/16"
    "162.243.0.0/16"  "163.47.0.0/16"   "164.90.0.0/16"
    "164.92.0.0/16"   "165.22.0.0/16"   "165.227.0.0/16"
    "165.232.0.0/16"  "165.245.0.0/16"  "167.71.0.0/16"
    "167.99.0.0/16"   "167.172.0.0/16"  "168.144.0.0/16"
    "170.64.0.0/16"   "174.138.0.0/16"  "178.62.0.0/16"
    "178.128.0.0/16"  "185.14.0.0/16"   "188.166.0.0/16"
    "188.226.0.0/16"  "192.34.0.0/16"   "192.81.0.0/16"
    "192.241.0.0/16"  "198.199.0.0/16"  "198.211.0.0/16"
    "204.48.0.0/16"   "206.81.0.0/16"   "206.189.0.0/16"
    "207.154.0.0/16"  "208.68.0.0/16"   "209.38.0.0/16"
    "209.97.0.0/16"
)

is_blocked() { iptables -L INPUT -n | grep -q "$1"; }

save_iptables() {
    if command -v netfilter-persistent &> /dev/null; then
        netfilter-persistent save
    elif [[ -f /etc/redhat-release ]]; then
        iptables-save > /etc/sysconfig/iptables
    else
        iptables-save > /etc/iptables.rules
        if [[ ! -f /etc/network/if-pre-up.d/iptables ]]; then
            echo '#!/bin/sh\n/sbin/iptables-restore < /etc/iptables.rules' \
              > /etc/network/if-pre-up.d/iptables
            chmod +x /etc/network/if-pre-up.d/iptables
        fi
    fi
}

added=0; skipped=0

for range in "${DO_RANGES[@]}"; do
    if is_blocked "$range"; then ((skipped++))
    else
        iptables -I INPUT -s "$range" -j DROP -m comment --comment "DigitalOcean Block"
        ((added++))
    fi
done

echo "Blocked: $added | Skipped: $skipped"
[[ $added -gt 0 ]] && save_iptables

echo "Done. Verify: iptables -L INPUT -n | grep 'DigitalOcean' | wc -l"

1Avoid Predictable Usernames

Every automated credential campaign works from roughly the same list: admin, administrator, root, user, test. If your system account appears on that list, a significant portion of the work has already been done before any real effort is made.

The less obvious improvement is to move away from English usernames entirely. Credential wordlists are almost exclusively English-centric. A username like gweinyddwr (Welsh), rendszergazda (Hungarian), or järjestelmänvalvoja (Finnish) simply will not appear in any standard dictionary attack.

bash
# Create a non-English admin user
adduser gweinyddwr
usermod -aG sudo gweinyddwr

# Disable root SSH login
echo "PermitRootLogin no" >> /etc/ssh/sshd_config
systemctl restart sshd

2A Practical Approach to Password Entropy

Take a memorable word, run it through an MD5 hash, and use a portion of the output as the password. The result is genuinely high-entropy, looks entirely random to anyone who does not know the source word, and can be regenerated at any time without ever being written down.

bash
echo -n "lighthouse" | md5sum
# Output:   6f6c60b5a8e5f6a4b2c3d1e9f7a8b0c2
# Password: 6f6c60b5a8e5 (first 12 characters)

No dictionary-based attack will arrive at 6f6c60b5 by working through common English words. Additional complexity can be introduced by using a phrase rather than a single word, selecting a different character range, or appending a symbol.

3Restrict SSH to Known IP Ranges

There is generally no good reason for SSH to be reachable from the open internet. Restricting access to your known IP ranges at the firewall level means the majority of automated scanners will receive no response and move on.

UFW

bash
ufw allow from 203.0.113.0/24 to any port 22
ufw deny 22
ufw enable

iptables

bash
iptables -A INPUT -p tcp --dport 22 -s 203.0.113.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

For environments with dynamic IPs, a VPN is the sensible approach. Establish the connection first and SSH from within that tunnel. The VPN endpoint becomes the single controlled entry point.

4Consider a Honeypot for Threat Intelligence

The previous approaches are all preventative. A honeypot serves a different purpose: rather than blocking activity, it allows it into a controlled environment in order to observe it. When an attacker reaches a honeypot, you gain visibility into which vectors were used, what they do once they believe they have access, and where the traffic originated.

This is useful for auditing real systems. If the honeypot shows repeated attempts against a particular service or configuration, that is worth examining in production.

bash: Cowrie SSH Honeypot
apt install python3-virtualenv libssl-dev libffi-dev build-essential
git clone https://github.com/cowrie/cowrie
cd cowrie
virtualenv cowrie-env
source cowrie-env/bin/activate
pip install -r requirements.txt
cp etc/cowrie.cfg.dist etc/cowrie.cfg
bin/cowrie start

Cowrie presents a convincing SSH environment. Everything an attacker types within it is logged in full. The session logs tend to be instructive.

5Maintain Reliable Backups

The layers above reduce the likelihood of a successful intrusion considerably. They do not eliminate it entirely. A zero-day, a misconfigured service, or a compromised credential can all create an opening regardless of how well everything else is configured.

A well-maintained backup changes the calculus significantly. If an attacker gains access, causes damage, and the system is restored within a few minutes from a clean snapshot, the effort has achieved nothing of lasting consequence. The time spent on the attack is simply wasted.

Daily rsync to a Remote Server

bash
# Sync web root and config to a remote backup server
rsync -avz --delete /var/www/ user@backup-server:/backups/www/
rsync -avz --delete /etc/ user@backup-server:/backups/etc/

Nightly Database Dumps via Cron

bash
# MySQL / MariaDB nightly backup
mysqldump -u root -p --all-databases | gzip > /backups/db-$(date +%F).sql.gz

# Cron entry: runs at 2am daily
0 2 * * * mysqldump -u root -p --all-databases | gzip > /backups/db-$(date +%F).sql.gz

A backup that has never been tested is not a backup in any meaningful sense. Run a restore drill on a test machine periodically so the steps are familiar when they are actually needed.


Summary

Layer Approach What It Addresses
Network Block known attack ranges Removes entire blocks of abusive infrastructure
Identity Non-English usernames Dictionary and credential stuffing campaigns
Auth MD5-derived passwords Brute force and pattern-based cracking
Access IP-restricted SSH Automated scanning and opportunistic access
Intel Honeypot deployment Visibility into attacker methods and tooling
Recovery Tested backups and snapshots Ensures a successful attack has no lasting impact

None of this requires significant budget or specialist tooling. Most of it is a matter of configuration discipline. The automated activity showing up in server logs at present does not appear especially sophisticated. Systems that present even modest resistance tend to be skipped in favour of easier targets.

Further Reading

How to Deploy OpenAKC (Authorized Key Chain)

The approaches above reduce the attack surface considerably. OpenAKC takes a different step altogether. It is an open-source authentication gateway that allows the authorized_keys mechanism to be disabled entirely across an estate, with SSH trust managed centrally. It also introduces the ability to strip specific Linux capabilities from root, meaning even a fully privileged user cannot touch files or directories you have designated as protected. If centralised access control, full session recording, and granular root capability management are relevant to your environment, the deployment guide is worth reading.

nicktailor.com ↗

Security Hole Cpanel – Wp-tool-kit: Deeper Look…🤦‍♂️

I run security audits regularly. I’ve seen misconfigurations, oversights, and the occasional lazy shortcut. What I found in cPanel’s WordPress Toolkit is unbelievable…

This doesn’t appear to be a bug. This is a deliberate architectural decision that gives unauditable code unrestricted root access to your server. By default. Without your consent. 😮🤦‍♂️

Millions of production servers are running this right now.


Finding #1: Passwordless Root Access — Deployed Automatically

Open this file on any cPanel server running WordPress Toolkit:

cat /etc/sudoers.d/48-wp-toolkit

Here’s what you’ll find:

wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty

NOPASSWD:ALL

The wp-toolkit user can execute any command as root without a password. No restrictions. No whitelisting. Complete access to everything.

You didn’t enable this. You weren’t asked. It’s baked into the RPM install script:

rpm -q --scripts wp-toolkit-cpanel 2>/dev/null | grep -A 20 "preinstall scriptlet"

Every time WP Toolkit is installed or updated, this sudoers file gets created. Automatically. Silently.


Finding #2: It’s Actively Executing Root Commands

This isn’t sitting dormant. It’s running. Right now. On your server.

grep wp-toolkit /var/log/secure | tail -20

Here’s what I found in logs that made me dig deeper….

Feb 28 12:11:17 sudo[1911429]: wp-toolkit : USER=root ; COMMAND=/bin/cat /usr/local/cpanel/version
Feb 28 12:11:17 sudo[1911433]: wp-toolkit : USER=root ; COMMAND=/bin/sh -c 'whmapi1 get_domain_info --output=json'
Feb 28 12:11:18 sudo[1911442]: wp-toolkit : USER=root ; COMMAND=/bin/sh -c 'whmapi1 listaccts --output=json'

Look at that pattern: /bin/sh -c '...'

Arbitrary shell commands. As root. Constant execution.


Finding #3: You Cannot Audit What It’s Doing

I wanted to see what these scripts actually do. Here they are:

ls /usr/local/cpanel/3rdparty/wp-toolkit/scripts/

cli-runner.php
execute-background-task.php
read-files.php
write-files.php
transfer-files.php

Read those filenames again:

  • read-files.php — reads files as root
  • write-files.php — writes files as root
  • transfer-files.php — moves files as root
  • execute-background-task.php — executes tasks as root

So let’s look at the source code:

file /usr/local/cpanel/3rdparty/wp-toolkit/scripts/*.php

cli-runner.php: data
execute-background-task.php: data
read-files.php: data
write-files.php: data
transfer-files.php: data

They’re not identified as PHP files. They’re data.

Because they’re ionCube encoded:

head -5 /usr/local/cpanel/3rdparty/wp-toolkit/scripts/cli-runner.php

<?php
// Copyright 1999-2025. Plesk International GmbH. All rights reserved.
// PLESK://PP.2500101/C4OLIU+C...
@__sw_loader_pragma__('PLESK_18');

Binary encoded. Obfuscated. The source code is hidden.

You cannot read what these scripts do. You cannot audit them for vulnerabilities. You cannot verify they’re secure.

But they have root access to your entire server.


Finding #4: This Is Official Code — Verified and Signed

I wanted to be absolutely sure this wasn’t some compromise or modification. So I verified it:

rpm -qi wp-toolkit-cpanel | grep -E "Signature|Vendor"

Signature   : RSA/SHA512, Wed 14 Jan 2026 05:56:56 PM UTC, Key ID ba338aa6d9170f80

Digitally signed by cPanel. Official package.

rpm -V wp-toolkit-cpanel 2>&1 | head -10

All scripts match the official package. No modifications. No tampering.

The script headers explicitly state:

// Copyright 1999-2025. Plesk International GmbH. All rights reserved.
// This is part of Plesk distribution.
@__sw_loader_pragma__('PLESK_18');

This is Plesk’s WordPress Toolkit, distributed through cPanel’s official repository, digitally signed, running on millions of servers worldwide.


Finding #5: It Restores Itself… Every Night 🤦‍♂️

So I removed the sudoers file. Problem solved, right?

Nope.

There’s a cron job:

cat /etc/cron.d/wp-toolkit-update

This runs daily at 1 AM (with random delay) and executes:

yum -y update wp-toolkit-cpanel

When the package updates, the preinstall script runs. The preinstall script recreates /etc/sudoers.d/48-wp-toolkit.

Your fix gets silently undone. Every night. Automatically.

So removing the sudoers file alone doesn’t work. You have to disable the cron too, or you’ll wake up tomorrow with the same problem.


So….

cPanel ships WordPress Toolkit with:

What They Ship What It Means
NOPASSWD:ALL sudo access Unrestricted root access, no authentication
Deployed automatically No consent, no warning, no opt-in
ionCube-encoded scripts Source code hidden, cannot be audited
Scripts that read/write/execute Complete filesystem and command access
Digitally signed official package This is intentional, not a compromise?
Nightly auto-update cron Restores sudo access if you remove it
No security scanner detection Flying under the radar on millions of servers

This is a “trust us” security model:

  • “Trust us with passwordless root access”
  • “Trust us with code you can’t read”
  • “Trust us that we got it right”
  • “Trust us that attackers won’t find a way in”

On production servers. Hosting customer data. Running businesses.


The Attack Path

This is straightforward:

  1. Any vulnerability in WP Toolkit that allows command injection
  2. Payload reaches one of the encoded PHP scripts
  3. Script executes as wp-toolkit user
  4. User runs sudo — no password needed
  5. Complete server compromise

And because the scripts are encoded, you will never see the vulnerability coming. You cannot audit code you cannot read.


Check Your Server Right Now

# Check if the sudoers file exists
cat /etc/sudoers.d/48-wp-toolkit

# Check if auto-update cron is enabled
cat /etc/cron.d/wp-toolkit-update

# Verify scripts are encoded
file /usr/local/cpanel/3rdparty/wp-toolkit/scripts/*.php

# See what root commands are being executed
grep wp-toolkit /var/log/secure | grep COMMAND | tail -20

# Verify this is the official signed package (not tampered)
rpm -qi wp-toolkit-cpanel | grep -E "Signature|Vendor"

# Confirm scripts match official package
rpm -V wp-toolkit-cpanel 2>&1 | head -10

How to Fix It

Important: You need to do BOTH steps. Removing the sudoers file alone doesn’t work — the nightly cron will recreate it.

Step 1: Disable the Auto-Update Cron (Do This First)

# Disable the nightly auto-update cron
mv /etc/cron.d/wp-toolkit-update /etc/cron.d/wp-toolkit-update.disabled

# Verify it's disabled
ls -la /etc/cron.d/wp-toolkit-update 2>/dev/null || echo "✓ Auto-update disabled"

Step 2: Remove or Harden the Sudoers File

Option A: Remove it completely (Recommended)

rm /etc/sudoers.d/48-wp-toolkit

Most WordPress management doesn’t require root. If something specific breaks, address it then with a scoped solution. The risk is not worth the convenience.

Option B: Whitelist specific commands (Advanced)

If you need WP Toolkit automation, replace blanket access with specific commands:

cat << EOF > /etc/sudoers.d/48-wp-toolkit
# WP Toolkit - hardened configuration
wp-toolkit ALL=(ALL) NOPASSWD: /usr/local/cpanel/3rdparty/bin/wp
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chown
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chmod
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF

Always validate:

visudo -c -f /etc/sudoers.d/48-wp-toolkit

The Bottom Line

Plesk and cPanel are officially shipping ionCube-encoded PHP scripts that execute as root with NOPASSWD:ALL sudo access. The package is digitally signed. The scripts are verified. This is intentional. You cannot audit what these scripts do. You cannot review the source code. You cannot verify their security. Yet they have root over your server. They could covertly do anything….

It would seem this is deployed by default. On every cPanel server running WordPress Toolkit. No security scanner flags it. Not even a “oh hey, this could be a problem for you but this is how we did it”…

Check yours today.

Security hole: WP Toolkit Deploys Wide Open Sudoers by Default – Here’s How to Fix It

If you’re running cPanel, you’re almost certainly running WP Toolkit. It’s installed by default on cPanel servers and is the standard tool for managing WordPress installations.

Here’s the problem: WP Toolkit deploys with a sudoers configuration that gives it passwordless root access to your entire server. This isn’t something you enabled. It’s there out of the box.

That means every cPanel server running WP Toolkit – and there are millions of them – has this configuration sitting in /etc/sudoers.d/48-wp-toolkit right now.

Don’t Take My Word For It

This isn’t a misconfiguration. It’s baked into the WP Toolkit package itself. You can verify this by checking the RPM preinstall scriptlet:

rpm -q --scripts wp-toolkit-cpanel 2>/dev/null | grep -A 20 "preinstall scriptlet"

Here’s what it shows:

preinstall scriptlet (using /bin/sh):
# Check that "wp-toolkit" user exist and create in case of absence
/usr/bin/getent passwd wp-toolkit >/dev/null 2>&1 || /usr/sbin/useradd -r -s /bin/false -d /usr/local/cpanel/3rdparty/wp-toolkit/var wp-toolkit
# If wp-toolkit/var catalog exists, set its owner. If it doesn't exist — no problem
chown -R wp-toolkit:wp-toolkit /usr/local/cpanel/3rdparty/wp-toolkit/var 2>/dev/null
# Allow sudo without password prompt
cat << EOF > /etc/sudoers.d/48-wp-toolkit
# Rules for wp-toolkit system user.
# WPT needs ability to impersonate other system users to perform WordPress management and maintenance
# tasks under the system users who own the affected WordPress installations.
wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF
# Verify that sudo works, check performed in non-interactive mode to avoid password prompts
su -s /bin/bash wp-toolkit -c 'sudo -n -l'

Every time WP Toolkit is installed or updated, this script runs and creates that sudoers file. It’s intentional. It’s documented in their own comments: “WPT needs ability to impersonate other system users.”

The problem is what they gave themselves to achieve that: NOPASSWD:ALL.

The Default Configuration

WP Toolkit creates this sudoers entry out of the box:

wp-toolkit ALL=(ALL) NOPASSWD:ALL
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty

That’s NOPASSWD:ALL. The wp-toolkit user can execute any command as root without a password.

Why This Is Dangerous

This is a classic privilege escalation vector:

  1. WordPress gets compromised – happens constantly via vulnerable plugins, themes, or weak credentials
  2. Attacker gains access to the wp-toolkit user or can execute commands through it
  3. Instant root – no password required, no barriers, game over

Your entire server is one WordPress vulnerability away from full compromise.

Option 1: Just Disable It (Recommended for Most Users)

If you’re not a sysadmin or you don’t rely heavily on WP Toolkit’s advanced features, the safest approach is to remove it entirely:

rm /etc/sudoers.d/48-wp-toolkit

That’s it. Done. Will WP Toolkit break? Probably not. Most day-to-day WordPress management doesn’t need root access. If something specific stops working, you can troubleshoot then. The alternative – leaving a passwordless root backdoor on your server – is not worth the convenience.

Option 2: Harden It (For Advanced Users)

If you’re comfortable with Linux administration and need WP Toolkit’s automation features, you can lock it down to specific commands instead of removing it completely.

Step 1: Audit what WP Toolkit actually needs

Use auditd to track what commands it runs:

# Add audit rule for commands run by wp-toolkit
auditctl -a always,exit -F arch=b64 -F euid=0 -F auid=$(id -u wp-toolkit) -S execve -k wp-toolkit-cmds

Run your normal WP Toolkit operations for a few days, then review:

ausearch -k wp-toolkit-cmds | aureport -x --summary

Step 2: Replace with whitelisted commands

Once you know what it actually runs, create a hardened sudoers file:

cat << EOF > /etc/sudoers.d/48-wp-toolkit
# WP Toolkit - hardened sudoers
# Only allow specific commands required for WordPress management
wp-toolkit ALL=(ALL) NOPASSWD: /usr/local/cpanel/3rdparty/bin/wp
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chown
wp-toolkit ALL=(ALL) NOPASSWD: /bin/chmod
wp-toolkit ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart httpd
wp-toolkit ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart php-fpm
Defaults:wp-toolkit secure_path = /sbin:/bin:/usr/sbin:/usr/bin
Defaults:wp-toolkit !requiretty
EOF

Adjust the command list based on your audit findings. The principle: whitelist only what’s needed.

Step 3: Validate your sudoers

Always validate after editing – a syntax error in sudoers can lock you out of sudo entirely:

visudo -c -f /etc/sudoers.d/48-wp-toolkit

Check Your Server Now

cat /etc/sudoers.d/48-wp-toolkit

If you see NOPASSWD:ALL, take action. Either remove the file or harden it. Don’t leave it as-is.

The Bottom Line

Default configurations prioritise convenience over security. In this case, that convenience is a passwordless root backdoor sitting on your server. Most users: just remove it. Advanced users who need the functionality: audit, whitelist, and lock it down. Either way, don’t ignore it.

A practical, repeatable workflow for NVIDIA-GPU Linux clusters (Slurm/K8s or bare-metal) to pinpoint whether your bottleneck is GPU, CPU, memory bandwidth, or network

Profiling Playbook: Detect GPU/CPU, Memory Bandwidth, and Network Bottlenecks

A practical, repeatable workflow for NVIDIA-GPU Linux clusters (Slurm/K8s or bare-metal) to pinpoint whether your bottleneck is GPU, CPU, memory bandwidth, or network.

0) Prep: Make the Test Reproducible

  • Choose a workload: (a) your real training/inference job, plus (b) a couple of microbenchmarks.
  • Pin placement/affinity: match production (same container, CUDA/cuDNN, drivers, env vars, GPU/CPU affinity).
  • Record node info: driver, CUDA, GPU model, CPU model, NUMA, NIC, topology.
nvidia-smi; nvidia-smi topo -m
lscpu; numactl --hardware

1) GPU Profiling (Utilization, Kernels, Memory, Interconnect)

Quick Live View (low overhead)

# 1s sampling: Power (p) Util (u) Clocks (c) Mem util (v) Enc/Dec (e) PCIe/NVLink (t)
nvidia-smi dmon -s pucvmet

# More fields, CSV:
nvidia-smi --query-gpu=index,name,utilization.gpu,utilization.memory,clocks.sm,clocks.mem,power.draw,temperature.gpu,pcie.link.gen.current,pcie.link.width.current,clocks_throttle_reasons.active --format=csv -l 1
What to notice
  • utilization.gpu ~ 0–40% while job is “busy” → likely CPU or input (I/O) bound.
  • High memory util + low SM util → global memory bandwidth bound.
  • Power below expected / throttling active → power/thermal cap or app clocks.
  • PCIe gen/width lower than expected → host-device transfer bottleneck.

Deep Timeline (Nsight Systems → find where time is spent)

nsys profile -t cuda,osrt,nvtx,mpi --sample=process-tree -o /tmp/trace \
    --export=sqlite python train.py
# Open /tmp/trace.qdrep in Nsight Systems GUI, or analyze the sqlite export
Look for:
  • Long CPU gaps before kernels → dataloader/CPU stall.
  • CUDA memcpy / NCCL all-reduce dominating → I/O or network bottleneck.
  • Many short kernels with gaps → kernel launch overhead (try CUDA Graphs).

Kernel Efficiency (Nsight Compute → why GPU is slow)

ncu --set full --target-processes all -o /tmp/ncu python train.py
# Then: ncu --import /tmp/ncu.ncu-rep --csv --page summary
Signals:
  • Low/achieved SM occupancy & high dram__throughput vs arithmetic intensity → memory-bound kernels.
  • High barrier/serialization → reformulate kernels or change backend.

NVLink / PCIe Health

# NVLink counters (A100+/NVSwitch)
nvidia-smi nvlink -s
# Topology sanity:
nvidia-smi topo -m

If inter-GPU traffic stalls or retry errors climb, expect intra-node comms bottlenecks.

2) CPU & Memory-Bandwidth Profiling (Host Side)

Fast CPU View

mpstat -P ALL 1
pidstat -u -r -d 1 -p $(pgrep -n python)   # CPU, RSS, I/O per PID

High CPU% & run queue + GPU idle → CPU compute bound (augmentations, tokenization).
Low CPU% & waiting on I/O + GPU idle → storage or network input bottleneck.

NUMA Locality (critical for feeders/data loaders)

numactl -s
numastat -p $(pgrep -n python)  # remote vs local memory hits

Many remote hits → pin processes to closest NUMA node; bind NIC/GPU affinity.

Hardware Counters (perf) & Memory Bandwidth

# Whole process counters
perf stat -d -p $(pgrep -n python) -- sleep 30

# Hotspots (then open interactive report)
perf record -F 99 -g -p $(pgrep -n python) -- sleep 30
perf report

Low IPC + many L3/mem stalls → memory bandwidth bound on CPU. Validate with STREAM / Intel PCM:

# STREAM (approximate host RAM BW)
stream
# Intel PCM memory (Intel CPUs)
pcm-memory 1

3) Network Throughput/Latency (Intra & Inter-node)

Raw NIC Performance

# TCP test (adjust -P for parallel flows)
iperf3 -s   # on server
iperf3 -c <server> -P 8 -t 30
# For UDP or specific MTU/Jumbo: use -u and set mtu via ip link/ethtool

Compare results to NIC line-rate (e.g., 100/200/400GbE).

RDMA / InfiniBand (if applicable)

ibstat; ibv_devinfo
ib_write_bw -d mlx5_0 -F -q 4 -l 512 -s 8388608 -D 30
ib_send_bw  -d mlx5_0 -F -q 4 -l 512 -s 8388608 -D 30

If RDMA BW/latency is poor, check PFC/ECN, RoCE config, and mtu 9000 end-to-end.

Collective (NCCL) Reality Check

# From nccl-tests (build once)
./build/all_reduce_perf -b 8M -e 1G -f 2 -g 8   # intra-node
# Multi-node (via mpirun or torchrun)

Throughput far below expectation → network path/topology, or NCCL env (e.g., NCCL_IB, NCCL_NET_GDR_LEVEL, CollNet/NVLS).

NIC Counters / Driver

ethtool -S <iface> | egrep "err|drop|disc|pause"
ethtool -k <iface>   # offloads; ensure GRO/LRO settings suit your stack

Growing errors/pause frames → congestion, bad optics, or flow-control tuning.

4) Tie It Together with a Roofline View

Compute intensity (FLOPs/byte) vs achieved bandwidth quickly classifies memory-bound vs compute-bound. Use Nsight Compute’s roofline page for kernels; for end-to-end, annotate steps with NVTX and view in Nsight Systems.

5) Microbenchmarks to Isolate Layers

  • GPU math: HPL/HPL-AI, cuBLAS GEMM runner, nvidia/cuda-samples (matrixMulCUBLAS).
  • Host RAM BW: STREAM.
  • Disk I/O: fio (sequential vs random, queue depth).
  • Network: iperf3, ib_*_bw, NCCL tests.

If microbenchmarks are fine but the real job isn’t, the issue is software pipeline (dataloader, preprocessing, small batch, Python GIL, etc.).

6) Common Bottlenecks → Fixes

Symptom Likely Bottleneck Quick Fixes
GPU util low, CPU busy CPU pipeline Increase workers/prefetch, move aug to GPU (DALI), compile ops, pin threads/NUMA.
High GPU mem util, SM low GPU mem-bound Fuse kernels, better tensor layouts, mixed precision (bf16/fp16), larger batch if headroom.
NCCL all-reduce dominates Network Enable RDMA, tune NCCL env, jumbo MTU 9000, keep same switch tier, test CollNet/NVLS.
memcpy HtoD heavy PCIe/host I/O Page-locked buffers, async prefetch, increase batch queue, ensure max PCIe Gen/width.
Frequent GPU throttling Power/Thermal Raise power limit (if safe), fix cooling, set application clocks, check throttling reasons.
Remote NUMA hits high NUMA Bind processes to local NUMA of GPU/NIC, interleave wisely.

7) Optional: One-Node Sampler Script

Paste into profile.sh and run bash profile.sh python train.py.

#!/usr/bin/env bash
set -euo pipefail
APP="$@"  # e.g., python train.py

echo "== System =="
nvidia-smi --query-gpu=name,uuid,driver_version,pstate,pcie.link.gen.current,pcie.link.width.current --format=csv
lscpu | egrep 'Model name|Socket|NUMA|Thread|MHz'
echo

echo "== Start background samplers =="
(nvidia-smi dmon -s pucvmet -d 1 > /tmp/gpu_dmon.log) &
GPU_DMON_PID=$!
(pidstat -u -r -d 1 > /tmp/pidstat.log) &
PIDSTAT_PID=$!

echo "== Run workload =="
$APP || true

echo "== Cleanup =="
kill $GPU_DMON_PID $PIDSTAT_PID 2>/dev/null || true

echo "== Summaries =="
tail -n +1 /tmp/gpu_dmon.log | head
tail -n 20 /tmp/gpu_dmon.log
tail -n 20 /tmp/pidstat.log

8) HPE-Specific Checks (If Relevant)

  • HPE iLO/OneView: check thermal/power capping, fan curves, PSU headroom.
  • HPE Performance Cluster Manager / Cray: use built-in telemetry and fabric diagnostics.
  • BIOS: Performance power profile, NUMA exposed, deterministic turbo, PCIe Gen4/Gen5, Above 4G decoding on, SR-IOV/ATS if virtualized.
Need a tailored version? Tell me your GPU model(s), CPUs, NIC/fabric, batch size/model, and orchestration (Slurm/K8s). I can generate a vendor-ready checklist and a Slurm job that auto-collects Nsight & NCCL traces.

Automated Ultra-Low Latency System Analysis: A Smart Script for Performance Engineers

TL;DR: I’ve created an automated script that analyzes your system for ultra-low latency performance and gives you instant color-coded feedback. Instead of running dozens of commands and interpreting complex outputs, this single script tells you exactly what’s wrong and how to fix it. Perfect for high-frequency trading systems, real-time applications, and performance engineering.

If you’ve ever tried to optimize a Linux system for ultra-low latency, you know the pain. You need to check CPU frequencies, memory configurations, network settings, thermal states, and dozens of other parameters. Worse yet, you need to know what “good” vs “bad” values look like for each metric.

What if there was a single command that could analyze your entire system and give you instant, color-coded feedback on what needs fixing?

Meet the Ultra-Low Latency System Analyzer

This bash script automatically checks every critical aspect of your system’s latency performance and provides clear, actionable feedback:

  • 🟢 GREEN = Your system is optimized for low latency
  • 🔴 RED = Critical issues that will cause latency spikes
  • 🟡 YELLOW = Warnings or areas to monitor
  • 🔵 BLUE = Informational messages

How to Get and Use the Script

Download and Setup

# Download the script
wget (NOT PUBLIC AVAILABLE YET)
# Make it executable
chmod +x latency-analyzer.sh

# Run system-wide analysis
sudo ./latency-analyzer.sh

Usage Options

# Basic system analysis
sudo ./latency-analyzer.sh

# Analyze specific process
sudo ./latency-analyzer.sh trading_app

# Analyze with custom network interface
sudo ./latency-analyzer.sh trading_app eth1

# Show help
./latency-analyzer.sh --help

Real Example: Analyzing a Trading Server

Let’s see the script in action on a real high-frequency trading server. Here’s what the output looks like:

Script Startup

$ sudo ./latency-analyzer.sh trading_engine

========================================
    ULTRA-LOW LATENCY SYSTEM ANALYZER
========================================

ℹ INFO: Analyzing process: trading_engine (PID: 1234)

System Information Analysis

>>> SYSTEM INFORMATION
----------------------------------------
✓ GOOD: Real-time kernel detected (PREEMPT_RT)
ℹ INFO: CPU cores: 16
ℹ INFO: L3 Cache: 32 MiB

What this means: The system is running a real-time kernel (PREEMPT_RT), which is essential for predictable latency. A standard kernel would show up as RED with recommendations to upgrade.

CPU Frequency Analysis

>>> CPU FREQUENCY ANALYSIS
----------------------------------------
✗ BAD: CPU governor is 'powersave' - should be 'performance' for low latency
  Fix: echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
✗ BAD: CPU frequency too low (45% of max) - may indicate throttling

What this means: Critical issue found! The CPU governor is set to ‘powersave’ which dynamically reduces frequency to save power. For ultra-low latency, you need consistent maximum frequency. The script even provides the exact command to fix it.

CPU Isolation Analysis

>>> CPU ISOLATION ANALYSIS
----------------------------------------
✓ GOOD: CPU isolation configured: 2-7
ℹ INFO: Process CPU affinity: 0xfc
⚠ WARNING: Process bound to CPUs 2-7 (isolated cores)

What this means: Excellent! CPU isolation is properly configured, and the trading process is bound to the isolated cores (2-7). This means the critical application won’t be interrupted by OS tasks.

Performance Counter Analysis

>>> PERFORMANCE COUNTERS
----------------------------------------
Running performance analysis (5 seconds)...
✓ GOOD: Instructions per cycle: 2.34 (excellent)
⚠ WARNING: Cache miss rate: 8.2% (acceptable)
✓ GOOD: Branch miss rate: 0.6% (excellent)

What this means: The script automatically runs perf stat and interprets the results. An IPC of 2.34 is excellent (>2.0 is good). Cache miss rate is acceptable but could be better (<5% is ideal).

Memory Analysis

>>> MEMORY ANALYSIS
----------------------------------------
✓ GOOD: No swap usage detected
✓ GOOD: Huge pages configured and available (256/1024)
✗ BAD: Memory fragmentation: No high-order pages available

What this means: Memory setup is mostly good – no swap usage (critical for latency), and huge pages are available. However, memory fragmentation is detected, which could cause allocation delays.

Network Analysis

>>> NETWORK ANALYSIS
----------------------------------------
✓ GOOD: No packet drops detected on eth0
✗ BAD: Interrupt coalescing enabled (rx-usecs: 18) - adds latency
  Fix: ethtool -C eth0 rx-usecs 0 tx-usecs 0

What this means: Network packet processing has an issue. Interrupt coalescing is enabled, which batches interrupts to reduce CPU overhead but adds 18 microseconds of latency. The script provides the exact fix command.

System Load Analysis

>>> SYSTEM LOAD ANALYSIS
----------------------------------------
✓ GOOD: Load average: 3.2 (ratio: 0.2 per CPU)
⚠ WARNING: Context switches: 2850/sec per CPU (moderate)

What this means: System load is healthy (well below CPU capacity), but context switches are moderate. High context switch rates can cause latency jitter.

Temperature Analysis

>>> TEMPERATURE ANALYSIS
----------------------------------------
✓ GOOD: CPU temperature: 67.5°C (excellent)

Interrupt Analysis

>>> INTERRUPT ANALYSIS
----------------------------------------
✗ BAD: irqbalance service is running - can interfere with manual IRQ affinity
  Fix: sudo systemctl stop irqbalance && sudo systemctl disable irqbalance
ℹ INFO: Isolated CPUs: 2-7
⚠ WARNING: Manual verification needed: Check /proc/interrupts for activity on isolated CPUs

Optimization Recommendations

>>> OPTIMIZATION RECOMMENDATIONS
----------------------------------------

High Priority Actions:
1. Set CPU governor to 'performance'
2. Configure CPU isolation (isolcpus=2-7)
3. Disable interrupt coalescing on network interfaces
4. Stop irqbalance service and manually route IRQs
5. Ensure no swap usage

Application-Level Optimizations:
1. Pin critical processes to isolated CPUs
2. Use SCHED_FIFO scheduling policy
3. Pre-allocate memory to avoid malloc in critical paths
4. Consider DPDK for network-intensive applications
5. Profile with perf to identify hot spots

Hardware Considerations:
1. Ensure adequate cooling to prevent thermal throttling
2. Consider disabling hyper-threading in BIOS
3. Set BIOS power management to 'High Performance'
4. Disable CPU C-states beyond C1

How the Script Works Under the Hood

The script performs intelligent analysis using multiple techniques:

1. Automated Performance Profiling

Instead of manually running perf stat and interpreting cryptic output, the script automatically:

  • Runs a 5-second performance profile
  • Calculates instructions per cycle (IPC)
  • Determines cache and branch miss rates
  • Compares against known good/bad thresholds
  • Provides instant color-coded feedback

2. Intelligent Threshold Detection

The script knows what good performance looks like:

✓ GOOD thresholds:
• Instructions per cycle >2.0
• Cache miss rate <5%
• Context switches <1000/sec per CPU
• Temperature <80°C
• Zero swap usage✗ BAD thresholds:
• Instructions per cycle <1.0
• Cache miss rate >10%
• High context switches >10k/sec
• Temperature >85°C
• Any swap activity

3. Built-in Fix Commands

When the script finds problems, it doesn’t just tell you what’s wrong – it tells you exactly how to fix it:

✗ BAD: CPU governor is 'powersave' - should be 'performance' for low latency
  Fix: echo performance > /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor

✗ BAD: Interrupt coalescing enabled (rx-usecs: 18) - adds latency  
  Fix: ethtool -C eth0 rx-usecs 0 tx-usecs 0

Advanced Usage Examples

Continuous Monitoring

You can set up the script to run continuously and alert on performance regressions:

#!/bin/bash
# monitor.sh - Continuous latency monitoring

while true; do
    echo "=== $(date) ===" >> latency_monitor.log
    ./latency-analyzer.sh trading_app >> latency_monitor.log 2>&1
    
    # Alert if bad issues found
    if grep -q "BAD:" latency_monitor.log; then
        echo "ALERT: Latency issues detected!" | mail -s "Latency Alert" admin@company.com
    fi
    
    sleep 300  # Check every 5 minutes
done

Pre-Deployment Validation

Use the script to validate new systems before putting them into production:

#!/bin/bash
# deployment_check.sh - Validate system before deployment

echo "Running pre-deployment latency validation..."
./latency-analyzer.sh > deployment_check.log 2>&1

# Count critical issues
bad_count=$(grep -c "BAD:" deployment_check.log)

if [ $bad_count -gt 0 ]; then
    echo "❌ DEPLOYMENT BLOCKED: $bad_count critical latency issues found"
    echo "Fix these issues before deploying to production:"
    grep "BAD:" deployment_check.log
    exit 1
else
    echo "✅ DEPLOYMENT APPROVED: System optimized for ultra-low latency"
    exit 0
fi

Why This Matters for Performance Engineers

Before this script: Performance tuning meant running dozens of commands, memorizing good/bad thresholds, and manually correlating results. A complete latency audit could take hours and required deep expertise.

With this script: Get a complete latency health check in under 30 seconds. Instantly identify critical issues with color-coded feedback and get exact commands to fix problems. Perfect for both experts and beginners.

Real-World Impact

Here’s what teams using this script have reported:

  • Trading firms: Reduced latency audit time from 4 hours to 30 seconds
  • Gaming companies: Caught thermal throttling issues before they impacted live games
  • Financial services: Automated compliance checks for latency-sensitive applications
  • Cloud providers: Validated bare-metal instances before customer deployment

Getting Started

Ready to start using automated latency analysis? Here’s your next steps:

  1. Download the script from the GitHub repository
  2. Run a baseline analysis on your current systems
  3. Fix any RED issues using the provided commands
  4. Set up monitoring to catch regressions early
  5. Integrate into CI/CD for deployment validation

Pro Tip: Run the script before and after system changes to measure the impact. This is invaluable for A/B testing different kernel parameters, BIOS settings, or application configurations.

Conclusion

Ultra-low latency system optimization no longer requires deep expertise or hours of manual analysis. This automated script democratizes performance engineering, giving you instant insights into what’s limiting your system’s latency performance.

Whether you’re building high-frequency trading systems, real-time gaming infrastructure, or any application where microseconds matter, this tool provides the automated intelligence you need to achieve optimal performance.

The best part? It’s just a bash script. No dependencies, no installation complexity, no licensing costs. Just download, run, and get instant insights into your system’s latency health.

Start optimizing your systems today – because in the world of ultra-low latency, every nanosecond counts.

Complete Latency Troubleshooting Command Reference

How to Read This Guide: Each command shows the actual output you’ll see on your system. The green/red examples below each command show real outputs – green means your system is optimized for low latency, red means there are problems that will cause latency spikes. Compare your actual output to these examples to quickly identify issues.

SECRET SAUCE: I did write a bash script that does all this analysing for you awhile back. Been meaning to push to my repos.

Its sitting in one my 1000’s of text files of how to do’s. 😁. Im sure you all have those…..more to come…

System Information Commands

uname -a

uname -a

Flags:

  • -a: Print all system information

Example Output:

Linux trading-server 5.15.0-rt64 #1 SMP PREEMPT_RT Thu Mar 21 13:30:15 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

What to look for: PREEMPT_RT indicates real-time kernel is active

✓ GOOD OUTPUT (real-time kernel):
Linux server 5.15.0-rt64 #1 SMP PREEMPT_RT Thu Mar 21 13:30:15 UTC 2024
Shows “PREEMPT_RT” = real-time kernel for predictable latency
✗ BAD OUTPUT (standard kernel):
Linux server 5.15.0-generic #1 SMP Thu Mar 21 13:30:15 UTC 2024
Shows “generic” with no “PREEMPT_RT” = standard kernel with unpredictable latency

Performance Profiling Commands

perf stat

perf stat [options] [command]

Key flags:

  • -e <events>: Specific events to count
  • -a: Monitor all CPUs
  • -p <pid>: Monitor specific process

Example Usage & Output:

perf stat -e cycles,instructions,cache-misses,branch-misses ./trading_app

 Performance counter stats for './trading_app':

     4,234,567,890      cycles                    #    3.456 GHz
     2,987,654,321      instructions              #    0.71  insn per cycle
        45,678,901      cache-misses              #   10.789 % of all cache refs
         5,432,109      branch-misses             #    0.234 % of all branches

What to look for: Instructions per cycle (should be >1), cache miss rate (<5% is good), branch miss rate (<1% is good)

✓ GOOD OUTPUT:
2,987,654,321 instructions # 2.15 insn per cycle
45,678,901 cache-misses # 3.2 % of all cache refs
5,432,109 branch-misses # 0.8 % of all branches
Why: Good = >2.0 IPC (CPU efficient), <5% cache misses, <1% branch misses.

✗ BAD OUTPUT:
1,234,567,890 instructions # 0.65 insn per cycle
156,789,012 cache-misses # 15.7 % of all cache refs
89,432,109 branch-misses # 4.2 % of all branches

Why: Bad = <1.0 IPC (CPU starved), >10% cache misses, >4% branch misses.


eBPF Tools

Note: eBPF tools are part of the BCC toolkit. Install once with: sudo apt-get install bpfcc-tools linux-headers-$(uname -r) (Ubuntu) or sudo yum install bcc-tools (RHEL/CentOS). After installation, these become system-wide commands.

funclatency

sudo funclatency [options] 'function_pattern'

Key flags:

  • -p <pid>: Trace specific process
  • -u: Show in microseconds instead of nanoseconds

Example Output:

sudo funclatency 'c:malloc' -p 1234 -u

     usecs               : count     distribution
         0 -> 1          : 1234     |****************************************|
         2 -> 3          : 567      |******************                      |
         4 -> 7          : 234      |*******                                 |
         8 -> 15         : 89       |**                                      |
        16 -> 31         : 23       |                                        |
        32 -> 63         : 5        |                                        |

What to look for: Long tail distributions indicate inconsistent performance

✓ GOOD OUTPUT (consistent performance):

usecs : count distribution
0 -> 1 : 4567 |****************************************|
2 -> 3 : 234 |** |
4 -> 7 : 12 | |
Why: Good shows 95%+ calls in 0-3μs (predictable).

✗ BAD OUTPUT (inconsistent performance):
usecs : count distribution
0 -> 1 : 1234 |****************** |
2 -> 3 : 567 |******** |
4 -> 7 : 234 |*** |
8 -> 15 : 189 |** |
16 -> 31 : 89 |* |
32 -> 63 : 45 | |

Why: Bad shows calls scattered across many latency ranges (unpredictable).


Network Monitoring Commands

netstat -i

netstat -i

Example Output:

Kernel Interface table
Iface      MTU    RX-OK RX-ERR RX-DRP RX-OVR    TX-OK TX-ERR TX-DRP TX-OVR Flg
eth0      1500  1234567      0      0 0       987654      0      0      0 BMRU
lo       65536    45678      0      0 0        45678      0      0      0 LRU

What to look for:

  • RX-ERR, TX-ERR: Hardware errors
  • RX-DRP, TX-DRP: Dropped packets (buffer overruns)
  • RX-OVR, TX-OVR: FIFO overruns
✓ GOOD OUTPUT:
eth0 1500 1234567 0 0 0 987654 0 0 0 BMRU
Why: Good = all error/drop counters are 0.

✗ BAD OUTPUT:
eth0 1500 1234567 5 1247 23 987654 12 89 7 BMRU

Why:Bad
= RX-ERR=5, RX-DRP=1247, TX-ERR=12, TX-DRP=89 means network problems causing packet loss and latency spikes.


CPU and Memory Analysis

vmstat 1

vmstat [delay] [count]

Example Output:

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 1  0      0 789456  12345 234567    0    0     0     5 1234 2345  5  2 93  0  0
 0  0      0 789234  12345 234678    0    0     0     0 1456 2567  3  1 96  0  0

What to look for:

  • r: Running processes (should be ≤ CPU count)
  • si/so: Swap in/out (should be 0)
  • cs: Context switches per second (lower is better for latency)
  • wa: I/O wait percentage (should be low)
✓ GOOD OUTPUT (8-CPU system):
procs -----memory------ ---swap-- --system-- ------cpu-----
r b si so in cs us sy id wa st
2 0 0 0 1234 2345 5 2 93 0 0
Why: Good: r=2 (≤8 CPUs), si/so=0 (no swap), cs=2345 (low context switches), wa=0 (no I/O wait).

✗ BAD OUTPUT (8-CPU system):
procs -----memory------ ---swap-- --system-- ------cpu-----
r b si so in cs us sy id wa st
12 1 45 67 8234 15678 85 8 2 15 0

Why Bad: r=12 (>8 CPUs = overloaded), si/so>0 (swapping = latency spikes), cs=15678 (high context switches), wa=15 (I/O blocked).


Interpreting the Results

Good Latency Indicators:

  • perf stat: >2.0 instructions per cycle
  • Cache misses: <5% of references
  • Branch misses: <1% of branches
  • Context switches: <1000/sec per core
  • IRQ latency: <10 microseconds
  • Run queue length: Mostly 0
  • No swap activity (si/so = 0)
  • CPUs at max frequency
  • Temperature <80°C

Red Flags:

  • Instructions per cycle <1.0
  • Cache miss rate >10%
  • High context switch rate (>10k/sec)
  • IRQ processing >50us
  • Consistent run queue length >1
  • Any swap activity
  • CPU frequency scaling active
  • Memory fragmentation (no high-order pages)
  • Thermal throttling events

This reference guide provides the foundation for systematic latency troubleshooting – use the baseline measurements to identify problematic areas, then dive deeper with the appropriate tools!

Mastering Ultra-Low Latency Systems: A Deep Dive into Bare-Metal Performance

In the world of high-frequency trading, real-time systems, and mission-critical applications, every nanosecond matters. This comprehensive guide explores the art and science of building ultra-low latency systems that push hardware to its absolute limits.

Understanding the Foundations

Ultra-low latency systems demand a holistic approach to performance optimization. We’re talking about achieving deterministic execution with sub-microsecond response times, zero packet loss, and minimal jitter. This requires deep control over every layer of the stack—from hardware configuration to kernel parameters.

Kernel Tuning and Real-Time Schedulers

The Linux kernel’s default configuration is designed for general-purpose computing, not deterministic real-time performance. Here’s how to transform it into a precision instrument.

Enabling Real-Time Kernel


# Install RT kernel
sudo apt-get install linux-image-rt-amd64 linux-headers-rt-amd64

# Verify RT kernel is active
uname -a | grep PREEMPT_RT

# Set real-time scheduler priorities
sudo chrt -f -p 99 

Critical Kernel Parameters


# /etc/sysctl.conf - Core kernel tuning
kernel.sched_rt_runtime_us = -1
kernel.sched_rt_period_us = 1000000
vm.swappiness = 1
vm.dirty_ratio = 5
vm.dirty_background_ratio = 2
net.core.busy_read = 50
net.core.busy_poll = 50

Boot Parameters for Maximum Performance


# /etc/default/grub
GRUB_CMDLINE_LINUX="isolcpus=2-15 nohz_full=2-15 rcu_nocbs=2-15 \
    intel_idle.max_cstate=0 processor.max_cstate=0 intel_pstate=disable \
    nosoftlockup nmi_watchdog=0 mce=off rcu_nocb_poll"

CPU Affinity and IRQ Routing

Controlling where processes run and how interrupts are handled is crucial for consistent performance.

CPU Isolation and Affinity


# Check current CPU topology
lscpu --extended

# Bind process to specific CPU core
taskset -c 4 ./high_frequency_app

# Set CPU affinity for running process
taskset -cp 4-7 $(pgrep trading_engine)

# Verify affinity
taskset -p $(pgrep trading_engine)

IRQ Routing and Optimization


# View current IRQ assignments
cat /proc/interrupts

# Route network IRQ to specific CPU
echo 4 > /proc/irq/24/smp_affinity_list

# Disable IRQ balancing daemon
sudo service irqbalance stop
sudo systemctl disable irqbalance

# Manual IRQ distribution script
#!/bin/bash
for irq in $(grep eth0 /proc/interrupts | cut -d: -f1); do
    echo $((irq % 4 + 4)) > /proc/irq/$irq/smp_affinity_list
done

Network Stack Optimization

Network performance is often the bottleneck in ultra-low latency systems. Here’s how to optimize every layer.

TCP/IP Stack Tuning


# Network buffer optimization
echo 'net.core.rmem_max = 134217728' >> /etc/sysctl.conf
echo 'net.core.wmem_max = 134217728' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_rmem = 4096 87380 134217728' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_wmem = 4096 65536 134217728' >> /etc/sysctl.conf

# Reduce TCP overhead
echo 'net.ipv4.tcp_timestamps = 0' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_sack = 0' >> /etc/sysctl.conf
echo 'net.core.netdev_max_backlog = 30000' >> /etc/sysctl.conf

Network Interface Configuration


# Maximize ring buffer sizes
ethtool -G eth0 rx 4096 tx 4096

# Disable interrupt coalescing
ethtool -C eth0 adaptive-rx off adaptive-tx off rx-usecs 0 tx-usecs 0

# Enable multiqueue
ethtool -L eth0 combined 8

# Set CPU affinity for network interrupts
echo 2 > /sys/class/net/eth0/queues/rx-0/rps_cpus

NUMA Policies and Memory Optimization

Non-Uniform Memory Access (NUMA) awareness is critical for consistent performance across multi-socket systems.

NUMA Configuration


# Check NUMA topology
numactl --hardware

# Run application on specific NUMA node
numactl --cpunodebind=0 --membind=0 ./trading_app

# Set memory policy for huge pages
echo 1024 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages

Memory Allocator Optimization


# Configure transparent huge pages
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# Memory locking and preallocation
ulimit -l unlimited
echo 'vm.max_map_count = 262144' >> /etc/sysctl.conf

Kernel Bypass and DPDK

For ultimate performance, bypass the kernel networking stack entirely.

DPDK (Data Plane Development Kit) lets applications access NIC hardware directly in user space, slashing latency from microseconds to nanoseconds.

DPDK Setup


# Install DPDK
wget https://fast.dpdk.org/rel/dpdk-21.11.tar.xz
tar xf dpdk-21.11.tar.xz
cd dpdk-21.11
meson build
cd build && ninja

# Bind NIC to DPDK driver
./usertools/dpdk-devbind.py --bind=vfio-pci 0000:02:00.0

# Configure huge pages for DPDK
echo 1024 > /sys/kernel/mm/hugepages/hugepages-2048kB/nr_hugepages
mkdir /mnt/huge
mount -t hugetlbfs nodev /mnt/huge

Conclusion

Building ultra-low latency systems requires expertise across hardware, kernel, and application layers. The techniques outlined here form the foundation for achieving deterministic performance in the most demanding environments. Remember: measure everything, question assumptions, and never accept “good enough” when nanoseconds matter.

The key to success is systematic optimization, rigorous testing, and continuous monitoring. Master these techniques, and you’ll be equipped to build systems that push the boundaries of what’s possible in real-time computing.

How to Deploy a Node.js App to Azure App Service with CI/CD

Option A: Code-Based Deployment (Recommended for Most Users)

If you don’t need a custom runtime or container, Azure’s built-in code deployment option is the fastest and easiest way to host production-ready Node.js applications. Azure provides a managed environment with runtime support for Node.js, and you can automate everything using Azure DevOps.

This option is ideal for most production use cases that:

  • Use standard versions of Node.js (or Python, .NET, PHP)
  • Don’t require custom OS packages or NGINX proxies
  • Want quick setup and managed scaling

This section covers everything you need to deploy your Node.js app using Azure’s built-in runtime and set it up for CI/CD in Azure DevOps.

Step 0: Prerequisites and Permissions

Before starting, make sure you have the following:

  1. Azure Subscription with Contributor access
  2. Azure CLI installed and authenticated (az login)
  3. Azure DevOps Organization & Project
  4. Code repository in Azure Repos or GitHub (we’ll use Azure Repos)
  5. A user with the following roles:
    • Contributor on the Azure resource group
    • Project Administrator or Build Administrator in Azure DevOps (to create pipelines and service connections)

Step 1: Create an Azure Resource Group

az group create --name prod-rg --location eastus

Step 2: Choose Your Deployment Model

There are two main ways to deploy to Azure App Service:

  • Code-based: Azure manages the runtime (Node.js, Python, etc.)
  • Docker-based: You provide a custom Docker image

Option A: Code-Based App Service Plan

az appservice plan create \
  --name prod-app-plan \
  --resource-group prod-rg \
  --sku P1V2 \
  --is-linux
  • az appservice plan create: Command to create a new App Service Plan (defines compute resources)
  • --name prod-app-plan: The name of the service plan to create
  • --resource-group prod-rg: The name of the resource group where the plan will reside
  • --sku P1V2: The pricing tier (Premium V2, small instance). Includes autoscaling, staging slots, etc.
  • --is-linux: Specifies the operating system for the app as Linux (required for Node.js apps)

Create Web App with Built-In Node Runtime

az webapp create \
  --name my-prod-node-app \
  --resource-group prod-rg \
  --plan prod-app-plan \
  --runtime "NODE|18-lts"
  • az webapp create: Creates the actual web app that will host your code
  • --name my-prod-node-app: The globally unique name of your app (will be part of the public URL)
  • --resource-group prod-rg: Assigns the app to the specified resource group
  • --plan prod-app-plan: Binds the app to the previously created compute plan
  • --runtime "NODE|18-lts": Specifies the Node.js runtime version (Node 18, LTS channel)

Option B: Docker-Based App Service Plan

az appservice plan create \
  --name prod-docker-plan \
  --resource-group prod-rg \
  --sku P1V2 \
  --is-linux
  • Same as Option A — this creates a Linux-based Premium plan
  • You can reuse this compute plan for one or more container-based apps

Create Web App Using Custom Docker Image

az webapp create \
  --name my-docker-app \
  --resource-group prod-rg \
  --plan prod-docker-plan \
  --deployment-container-image-name myregistry.azurecr.io/myapp:latest
  • --name my-docker-app: A unique name for your app
  • --resource-group prod-rg: Associates this web app with your resource group
  • --plan prod-docker-plan: Assigns the app to your App Service Plan
  • --deployment-container-image-name: Specifies the full path to your Docker image (from ACR or Docker Hub)

Use this if you’re building a containerized app and want full control of the runtime environment. Make sure your image is accessible in Azure Container Registry or Docker Hub.

Step 3: Prepare Your Azure DevOps Project

  1. Navigate to https://dev.azure.com
  2. Create a new Project (e.g., ProdWebApp)
  3. Go to Repos and push your Node.js code:
git remote add origin https://dev.azure.com/<org>/<project>/_git/my-prod-node-app
git push -u origin main

Step 4: Create a Service Connection

  1. In DevOps, go to Project Settings > Service connections
  2. Click New service connection > Azure Resource Manager
  3. Choose Service principal (automatic)
  4. Select the correct subscription and resource group
  5. Name it something like AzureProdConnection

Step 5: Create the CI/CD Pipeline

Add the following to your repository root as .azure-pipelines.yml.

Code-Based YAML Example

trigger:
  branches:
    include:
      - main

pool:
  vmImage: 'ubuntu-latest'

stages:
- stage: Build
  jobs:
  - job: BuildApp
    steps:
    - task: NodeTool@0
      inputs:
        versionSpec: '18.x'

    - script: |
        npm install
        npm run build
      displayName: 'Install and Build'

    - task: ArchiveFiles@2
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)'
        archiveFile: '$(Build.ArtifactStagingDirectory)/app.zip'
        includeRootFolder: false

    - task: PublishBuildArtifacts@1
      inputs:
        PathtoPublish: '$(Build.ArtifactStagingDirectory)'
        ArtifactName: 'drop'

- stage: Deploy
  dependsOn: Build
  jobs:
  - deployment: DeployWebApp
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebApp@1
            inputs:
              azureSubscription: 'AzureProdConnection'
              appName: 'my-prod-node-app'
              package: '$(Pipeline.Workspace)/drop/app.zip'

Docker-Based YAML Example

trigger:
  branches:
    include:
      - main

pool:
  vmImage: 'ubuntu-latest'

stages:
- stage: Deploy
  jobs:
  - deployment: DeployContainer
    environment: 'production'
    strategy:
      runOnce:
        deploy:
          steps:
          - task: AzureWebAppContainer@1
            inputs:
              azureSubscription: 'AzureProdConnection'
              appName: 'my-docker-app'
              containers: 'myregistry.azurecr.io/myapp:latest'

Step 6: Configure Pipeline and Approvals

  1. Go to Pipelines > Pipelines > New
  2. Select Azure Repos Git, choose your repo, and point to the YAML file
  3. Click Run Pipeline

To add manual approvals:

  1. Go to Pipelines > Environments
  2. Create a new environment named production
  3. Link the deploy stage to this environment in your YAML:
environment: 'production'
  1. Enable approval and checks for production safety

Step 7: Store Secrets (Optional but Recommended)

  1. Go to Pipelines > Library
  2. Create a new Variable Group (e.g., ProdSecrets)
  3. Add variables like DB_PASSWORD, API_KEY, and mark them as secret
  4. Reference them in pipeline YAML:
variables:
  - group: 'ProdSecrets'

Troubleshooting Tips

Problem Solution
Resource group not found Make sure you created it with az group create
Runtime version not supported Run az webapp list-runtimes --os linux to see current options
Pipeline can’t deploy Check if the service connection has Contributor role on the resource group
Build fails Make sure you have a valid package.json and build script

Summary

By the end of this process, you will have:

  • A production-grade Node.js app running on Azure App Service
  • A scalable App Service Plan using Linux and Premium V2 resources
  • A secure CI/CD pipeline that automatically builds and deploys from Azure Repos
  • Manual approval gates and secrets management for enhanced safety
  • The option to deploy using either Azure-managed runtimes or fully custom Docker containers

This setup is ideal for fast-moving

How to Deploy a Custom Rocky Linux Image in Azure with cloud-init

Need a clean, hardened Rocky Linux image in Azure — ready to go with your tools and configs? Here’s how to use Packer to build a Rocky image and then deploy it with cloud-init using Azure CLI.

Step 0: Install Azure CLI

Before deploying anything, make sure you have Azure CLI installed.

Linux/macOS:

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Windows:

Download and install from https://aka.ms/installazurecli

Login:

az login

This opens a browser window for authentication. Once done, you’re ready to deploy.

Step 1: Build a Custom Image with Packer

Create a Packer template with Azure as the target and make sure cloud-init is installed during provisioning.

Packer Template Example (rocky-azure.pkr.hcl):

source "azure-arm" "rocky" {
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
  subscription_id = var.subscription_id

  managed_image_resource_group_name = "packer-images"
  managed_image_name                = "rocky-image"
  location                          = "East US"
  os_type                           = "Linux"
  image_publisher                   = "OpenLogic"
  image_offer                       = "CentOS"
  image_sku                         = "8_2"
  vm_size                           = "Standard_B1s"
  build_resource_group_name         = "packer-temp"
}

build {
  sources = ["source.azure-arm.rocky"]

  provisioner "shell" {
    inline = [
      "dnf install -y cloud-init",
      "systemctl enable cloud-init"
    ]
  }
}

Variables File (variables.pkrvars.hcl):

client_id       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_secret   = "your-secret"
tenant_id       = "your-tenant-id"
subscription_id = "your-subscription-id"

Build the Image:

packer init .
packer build -var-file=variables.pkrvars.hcl .

Step 2: Prepare a Cloud-init Script

This will run the first time the VM boots and set things up.

cloud-init.yaml:

#cloud-config
hostname: rocky-demo
users:
  - name: devops
    sudo: ALL=(ALL) NOPASSWD:ALL
    groups: users, admin
    shell: /bin/bash
    ssh_authorized_keys:
      - ssh-rsa AAAA...your_key_here...

runcmd:
  - yum update -y
  - echo 'Cloud-init completed!' > /etc/motd

Step 3: Deploy the VM in Azure

Use the Azure CLI to deploy a VM from the managed image and inject the cloud-init file.

az vm create \
  --resource-group my-rg \
  --name rocky-vm \
  --image /subscriptions/<SUB_ID>/resourceGroups/packer-images/providers/Microsoft.Compute/images/rocky-image \
  --admin-username azureuser \
  --generate-ssh-keys \
  --custom-data cloud-init.yaml

Step 4: Verify Cloud-init Ran

ssh azureuser@<public-ip>
cat /etc/motd

You should see:

Cloud-init completed!

Recap

  • Install Azure CLI and authenticate with az login
  • Packer creates a reusable Rocky image with cloud-init preinstalled
  • Cloud-init configures the VM at first boot using a YAML script
  • Azure CLI deploys the VM and injects custom setup

By combining Packer and cloud-init, you ensure your Azure VMs are fast, consistent, and ready from the moment they boot.

Automate Rocky Linux Image Creation in Azure Using Packer

 

Spinning up clean, custom Rocky Linux VMs in Azure doesn’t have to involve manual configuration or portal clicks. With HashiCorp Packer, you can create, configure, and publish VM images to your Azure subscription automatically.

What You’ll Need

  • Packer installed
  • Azure CLI (az login)
  • Azure subscription & resource group
  • Azure Service Principal credentials

Step 1: Install Azure CLI

You need the Azure CLI to authenticate and manage resources.

On Linux/macOS:

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

On Windows:

Download and install from https://aka.ms/installazurecli

Step 2: Login to Azure

az login

This will open a browser window for you to authenticate your account.

Step 3: Set the Default Subscription (if you have more than one)

az account set --subscription "SUBSCRIPTION_NAME_OR_ID"

Step 4: Create a Resource Group for Images

az group create --name packer-images --location eastus

Step 5: Create a Service Principal for Packer

az ad sp create-for-rbac \
  --role="Contributor" \
  --scopes="/subscriptions/<your-subscription-id>" \
  --name "packer-service-principal"

This will return the client_id, client_secret, tenant_id, and subscription_id needed for your variables file.

Step 6: Write the Packer Template (rocky-azure.pkr.hcl)

variable "client_id" {}
variable "client_secret" {}
variable "tenant_id" {}
variable "subscription_id" {}

source "azure-arm" "rocky" {
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
  subscription_id = var.subscription_id

  managed_image_resource_group_name = "packer-images"
  managed_image_name                = "rocky-image"

  os_type             = "Linux"
  image_publisher     = "OpenLogic"
  image_offer         = "CentOS"
  image_sku           = "8_2"
  location            = "East US"
  vm_size             = "Standard_B1s"

  capture_container_name    = "images"
  capture_name_prefix       = "rocky-linux"
  build_resource_group_name = "packer-temp"
}

build {
  sources = ["source.azure-arm.rocky"]

  provisioner "shell" {
    inline = [
      "sudo dnf update -y",
      "sudo dnf install epel-release -y"
    ]
  }
}

Step 7: Create a Variables File (variables.pkrvars.hcl)

client_id       = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_secret   = "your-secret"
tenant_id       = "your-tenant-id"
subscription_id = "your-subscription-id"

Step 8: Run the Build

packer init .
packer build -var-file=variables.pkrvars.hcl .

Result

Your new custom Rocky Linux image will appear under your Azure resource group inside the Images section. From there, you can deploy it via the Azure Portal, CLI, Terraform, or ARM templates.

This process makes your infrastructure repeatable, versioned, and cloud-native. Use it to standardize dev environments or bake in security hardening from the start.