Analyze your ESP32 WiFi performance

5
(1)

Want to analyze your ESP32 WiFi performance? Measure available Wi-Fi networks and signal strength, then benchmark real-world TCP throughput. This tutorial provides both network scanning and download speed testing for your ESP32 projects.

Part 1: Wi-Fi signal strength

The first part of this tutorial is a basic Wi-Fi network scan, to measure available Wi-Fi networks and signal strength. Signal strength is measured by the RSSI (Received Signal Strength Indicator), which is the power level of a received Wi-Fi signal in dBm (decibels relative to 1 milliwatt).

The code below scans both 2.4 GHz and 5 GHz networks, displaying channel numbers and RSSI for each found network. The ESP32 then connects to your WiFi and continuously monitors RSSI—ideal for testing signal range or verifying antenna reception quality (gain)

C++
//
// ESP32 WiFi test program
// https://tutoduino.fr/
//

#include <WiFi.h>      // WiFi library for ESP32
#include <esp_wifi.h>  // ESP-IDF WiFi control functions
#include "secret.h"    // Contains WIFI_SSID and WIFI_PASSWORD definitions

void setup() {
  Serial.begin(115200);  // Initialize serial communication
  delay(1000);           // Small startup delay
  Serial.println("ESP32 WiFi Test - Tutoduino");

  scanWifi();       // Scan nearby 2.4GHz and 5GHz networks
  connectToWifi();  // Try to connect to your WiFi network
}

void loop() {
  // Check if the device is still connected to the WiFi network
  if (WiFi.status() == WL_CONNECTED) {
    int ch = WiFi.channel();   // Get current WiFi channel
    bool is5GHz = (ch >= 36);  // Channels 36+ correspond to 5GHz

    // Print IP address and signal info
    Serial.printf("Connected with IP: %s\n", WiFi.localIP().toString().c_str());
    Serial.printf("Channel: %d (%s GHz) | RSSI: %d dBm\n",
                  ch, is5GHz ? "5" : "2.4", WiFi.RSSI());
  } else {
    // If disconnected, try to reconnect automatically
    Serial.println("Disconnected, trying to reconnect...");
    WiFi.reconnect();
  }

  delay(5000);  // Wait before repeating the loop
}

void scanWifi() {
  int n;
  esp_err_t err;

  WiFi.mode(WIFI_STA);  // Set WiFi to station (client) mode

  // ----------- 5 GHz scan -----------
  Serial.println("Scanning 5 GHz WiFi networks...");
  err = esp_wifi_set_band_mode(WIFI_BAND_MODE_5G_ONLY);  // Restrict to 5 GHz

  if (err != ESP_OK) {
    Serial.println("Set 5G band failed!");
  }

  n = WiFi.scanNetworks();  // Perform the 5GHz scan
  for (int i = 0; i < n; ++i) {
    int ch = WiFi.channel(i);
    if (ch >= 36) {
      Serial.printf("[%d] %s (5G ch%d, %d dBm)\n",
                    i + 1, WiFi.SSID(i).c_str(), ch, WiFi.RSSI(i));
    } else {
      Serial.printf("[%d] %s (2.4G ch%d, %d dBm)\n",
                    i + 1, WiFi.SSID(i).c_str(), ch, WiFi.RSSI(i));
    }
  }
  WiFi.scanDelete();

  // ----------- 2.4 GHz scan -----------
  Serial.println("Scanning 2.4 GHz WiFi networks...");
  err = esp_wifi_set_band_mode(WIFI_BAND_MODE_2G_ONLY);  // Restrict to 2.4 GHz

  if (err != ESP_OK) {
    Serial.println("Set 2G band failed!");
  }
  n = WiFi.scanNetworks();  // Perform network scan
  for (int i = 0; i < n; ++i) {
    int ch = WiFi.channel(i);
    // Display basic info for each found network
    if (ch >= 36) {
      Serial.printf("[%d] %s (5G ch%d, %d dBm)\n",
                    i + 1, WiFi.SSID(i).c_str(), ch, WiFi.RSSI(i));
    } else {
      Serial.printf("[%d] %s (2.4G ch%d, %d dBm)\n",
                    i + 1, WiFi.SSID(i).c_str(), ch, WiFi.RSSI(i));
    }
  }
  WiFi.scanDelete();  // Free memory from scan results
}

// Function to connect the ESP32 to the provided WiFi network
void connectToWifi() {
  Serial.printf("Connecting to %s...\n", WIFI_SSID);
  WiFi.mode(WIFI_STA);                   // Set WiFi to station (client) mode
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);  // Start connection with credentials

  int attempts = 0;
  // Wait up to 20 attempts (about 10 seconds total)
  while (WiFi.status() != WL_CONNECTED && attempts < 20) {
    delay(500);
    Serial.print(".");
    attempts++;
  }

  // Report connection result
  Serial.println(WiFi.status() == WL_CONNECTED ? "\nConnected!" : "\nFailed to connect");
}

Example of output on Arduino IDE Serial Monitor showing 2.4G and 5G Wi-Fi networks. The Seeed Studio XIAO ESP32-C6 then connect to my Wi-Fi and display channel and RSSI:

Plaintext
17:30:34.782 -> ESP32 WiFi Test - Tutoduino
17:30:35.393 -> Scanning 5 GHz WiFi networks...
17:30:42.356 -> [1] MyWifi_5GHz (5G ch112, -68 dBm)
17:30:42.356 -> Scanning 2.4 GHz WiFi networks...
17:30:45.372 -> [1] MyWifi (2.4G ch11, -70 dBm)
17:30:45.372 -> [2] Freebox-0265C0 (2.4G ch6, -84 dBm)
17:30:45.372 -> [3] Maison Cabanon (2.4G ch2, -88 dBm)
17:30:45.372 -> [4] Jiggabox (2.4G ch6, -88 dBm)
17:30:45.372 -> [5] Jiggabox-invité (2.4G ch6, -89 dBm)
17:30:45.372 -> [6] Livebox-20B0 (2.4G ch6, -89 dBm)
17:30:45.372 -> [7] Livebox-30E0 (2.4G ch1, -91 dBm)
17:30:45.372 -> [8] WIFI7-YOU (2.4G ch1, -93 dBm)
17:30:45.372 -> [9] boxjack02 (2.4G ch2, -95 dBm)
17:30:45.372 -> [10] wifiRoom (2.4G ch6, -95 dBm)
17:30:45.372 -> Connecting to MyWifi...
17:30:45.885 -> ...
17:30:46.880 -> Connected!
17:30:46.880 -> Connected with IP: 192.168.1.31
17:30:46.880 -> Channel: 11 (2.4 GHz) | RSSI: -68 dBm
17:30:51.890 -> Connected with IP: 192.168.1.31
17:30:51.890 -> Channel: 11 (2.4 GHz) | RSSI: -69 dBm
17:30:56.875 -> Connected with IP: 192.168.1.31
17:30:56.875 -> Channel: 11 (2.4 GHz) | RSSI: -59 dBm
17:31:01.875 -> Connected with IP: 192.168.1.31
17:31:01.875 -> Channel: 11 (2.4 GHz) | RSSI: -59 dBm

In this example, you can see the effect of changing the antenna (at 17:30:56), which improved the RSSI from -69 dBm to -59 dBm.

Part 2: Wi-Fi throughput

In this test, we measure Wi-Fi throughput on ESP32 by transferring a 20 MB file from a local Linux web server to your ESP32.

Before going further, I remind you some concepts for the units of information:

  • Bit (b) → most basic unit of information (0 or 1)
  • Kilobit (kb ou kbit) → 1 kbit = 1 000 b
  • Megabit (Mb ou Mbit) → 1 Mbit = 1 000 000 b
  • Byte (B) → 1 B = 8 b
  • Kilobyte (kB) → 1 kB = 1 000 B
  • Kibibyte (KiB) → 1 KiB = 1 024 B
  • Megabyte (MB) → 1 MB = 1 000 x 1 000 B = 1 000 000 B
  • Mebibyte (MiB) → 1 MiB = 1 024 x 1 024 B = 1 048 576 B
1. Server Setup (Linux)

Create 20 MiB (Mebibyte) random data file with this command on the local Linux server:

Bash
dd count=20480 bs=1024 if=/dev/random of=/tmp/data

20 480 blocks × 1 024 bytes/block = 20,971,520 bytes = 20 × 1 048 576 bytes = 20 MiB = ~21 MB.

2. HTTP server

Start a simple Python HTTP server with this command executed on the local Linux server:

Bash
python3 -m http.server 8080 -d /tmp

You can test that local web server is operational with this command executed on the local Linux server:

Bash
tutoduino@F15:~$ curl -o /dev/null http://127.0.0.1:8080/data
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 20.0M  100 20.0M    0     0  2428M      0 --:--:-- --:--:-- --:--:-- 2500M

3. ESP32 Client

Compile and upload this code to your ESP32. It performs HTTP GET requests from ESP32 to download the 21 MB file from the local web server and calculates Wi-Fi throughput in kb/s.

C++
//
// ESP32 Wi-Fi throughput test program
//
// https://tutoduino.fr/
//

#include <WiFi.h>
#include <esp_wifi.h>  // ESP-IDF WiFi control functions
#include "secret.h"    // Contains WIFI_SSID and WIFI_PASSWORD definitions

// Global TCP client object
WiFiClient client;

void setup() {
  esp_err_t err;

  // Start serial console for debug output
  Serial.begin(115200);
  delay(1000);

  Serial.println("ESP32 WiFi Throughput Test - Tutoduino");

  // Connect to WiFi access point
  Serial.println("Connecting to WiFi...");
  WiFi.mode(WIFI_STA);

  // Restrict test to 2.4 GHz WiFi band
  err = esp_wifi_set_band_mode(WIFI_BAND_MODE_2G_ONLY);

  if (err != ESP_OK) {
    Serial.println("Set 2G band failed!");
  }

  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);

  // Wait until connected (blocking loop)
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println();
  Serial.println("WiFi connected!");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}

void loop() {
  // Only run the test if WiFi is still connected
  if (WiFi.status() == WL_CONNECTED) {
    const char* host = "192.168.1.22";  // Test server IP, change with your server IP!
    const int port = 8080;              // Test server port
    const char* path = "/data";         // Resource to download

    Serial.println("Starting throughput test...");

    unsigned long startTime = millis();
    int totalBytes = 0;

    // Open TCP connection to the server
    if (client.connect(host, port)) {
      Serial.println("Connected to server");

      // Send a minimal HTTP GET request
      client.print(String("GET ") + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");

      // Wait for HTTP response headers to arrive
      while (client.connected() && !client.available()) {
        delay(10);
      }

      // Skip HTTP headers (read until end of header line '\n')
      while (client.available() && client.read() != '\n') {
        // just discard header data
      }

      // Read HTTP body data and count bytes
      unsigned long lastActivity = millis();
      while (client.connected() || client.available()) {
        if (client.available()) {
          int bytesAvailable = client.available();
          totalBytes += bytesAvailable;

          // Read and discard data (we only count bytes)
          for (int i = 0; i < bytesAvailable; i++) {
            client.read();
          }

          // Update last activity time
          lastActivity = millis();
        }

        // Timeout after 2 seconds without any new data
        if (millis() - lastActivity > 2000) {
          break;
        }
      }

      // Close TCP connection
      client.stop();

      // Compute duration in seconds
      unsigned long endTime = millis();
      unsigned long duration = (endTime - startTime) / 1000;  // seconds

      // Avoid division by zero
      if (duration == 0) duration = 1;

      // Compute throughput in megabit/s : 1 byte = 8 bits ; 1 Mbit = 1000 bits * 1000 bits
      float speedMbitps = (totalBytes * 8.0 / (1000.0 * 1000.0)) / duration;

      Serial.print("Downloaded size: ");
      Serial.print(totalBytes);
      Serial.println(" bytes");

      Serial.print("Duration: ");
      Serial.print(duration);
      Serial.println(" s");

      Serial.print("Estimated throughput: ");
      Serial.print(speedMbitps, 2);
      Serial.println(" Mbit/s");
    } else {
      Serial.println("Failed to connect to server");
    }
  } else {
    Serial.println("WiFi disconnected");
  }

  // Wait 10 seconds before the next test
  delay(10000);
}

Replace host IP address (192.168.1.22) in the code with your Linux machine’s IP. Store your WIFI_SSID and WIFI_PASSWORD in a secret.h file.

Example of output on Arduino IDE Serial Monitor showing 7.29 Mbit/s throughput on ESP32:

Plaintext
17:03:56.822 -> ESP32 WiFi Throughput Test - Tutoduino
17:03:56.822 -> Connecting to WiFi...
17:03:58.441 -> ...
17:03:58.959 -> WiFi connected!
17:03:58.959 -> IP address: 192.168.1.31
17:03:58.959 -> Starting throughput test...
17:03:59.701 -> Connected to server
17:04:22.388 -> Downloaded size: 20971709 bytes
17:04:22.388 -> Duration: 23 s
17:04:22.388 -> Estimated throughput: 7.29 Mbit/s

Measurement consistency check and bottleneck analysis

To confirm the validity of the measurements, I carried out two additional tests with a larger file size of 105 MB.

Check performance of Python HTTP server

To check that Python HTTP server is not a bootleneck, I performed a local test on the Linux server.

Bash
steph@F15:~$ curl -o /dev/null http://127.0.0.1:8080/data
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  100M  100  100M    0     0  2164M      0 --:--:-- --:--:-- --:--:-- 2173M

Curl uses the binary prefix and bytes units, so M means MiB/s and not MB/s.

This output shows that throughput is very high with 2173 MiB/s = ~18 228 Mbit/s.

So the Python HTTP server is clearly not limiting .

Check network performance

I realized the same test on another PC in my local network on a larger file to have accurate value.

Bash
tutoduino@my-desktop:~$ curl -o /dev/null http://192.168.1.22:8080/data
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  100M  100  100M    0     0  12.4M      0  0:00:08  0:00:08 --:--:-- 12.8M

Result shows a throughput of 12.8 MiB/s = ~107.4 Mbit/s

It is consistent with the Wi-Fi card bitrate of my Linux server, as you can see below:

So the Wi-Fi network and the Wi-Fi connectivity on the Linux server is also not the limiting factor.

Conclusion

The measurements method presented above is accurate, confirming the ESP32’s throughput at approximately 7.63 Mbit/s.

This result aligns with typical ESP32 Wi-Fi performance in embedded IoT setups like ESPHome, where 7-8 Mbit/s is realistic for localhost transfers under optimal conditions.

I hope this tutorial was helpful! Please share your feedback by rating with the stars below or leaving a comment.

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 1

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?