Severe asymmetric UDP packet loss on XIAO ESP32-S3, while XIAO ESP32-C6 has 0% loss

Hello,

I am experiencing severe and repeatable asymmetric Wi-Fi packet loss with Seeed Studio XIAO ESP32-S3 boards.

The ESP32-S3 receives UDP packets from my Linux PC reliably, but a large percentage of packets transmitted from the ESP32-S3 to the PC are lost.

A Seeed Studio XIAO ESP32-C6 tested with the same UDP test achieved 0% packet loss in both directions.

Hardware and software

  • Board: Seeed Studio XIAO ESP32-S3
  • Comparison board: Seeed Studio XIAO ESP32-C6
  • Computer: Linux PC
  • Wi-Fi network: 2.4 GHz home router
  • Arduino networking API: WiFi and NetworkUDP
  • Wi-Fi mode: station mode
  • Wi-Fi sleep: disabled with WiFi.setSleep(false)
  • RSSI: about -48-50 dBm
  • Wi-Fi channel: channel 6
  • Distance from router: 2m
  • Number of XIAO ESP32-S3 boards tested: 6 XIAO ESP32 S3

Test method

I use two independent numbered UDP streams.

XIAO ESP32-S3 to PC

The ESP32-S3 sends one small UDP report every second.

Each report contains:

tx_sequence rx_count rx_last_sequence tx_failures last_error

The PC checks the ESP32 sequence numbers to detect missing packets.

PC to XIAO ESP32-S3

The Linux PC sends one numbered UDP packet every 100 ms.

The ESP32-S3 counts received packets and includes that count in its outgoing reports.

Test timing

Each normal test includes:

  • 60 seconds of bidirectional UDP traffic
  • 10 additional seconds receiving ESP32 reports after PC transmission stops

The drain period allows the final ESP32 receive counter to reach the PC and avoids falsely counting packets near the end of the test as lost.

Main symptom

The packet loss is strongly asymmetric.

Direction Typical result
XIAO ESP32-S3 to PC Approximately 50% to 75% loss
PC to XIAO ESP32-S3 Usually 0% to 1% loss
ESP32 local UDP send failures Usually 0

The packet rate is extremely low, so this does not appear to be UDP buffer overload or a throughput limitation.

Example result 1

=== device -> PC (ESP32 transmit path) ===
sequence span: 32..101 (70 attempts)
unique packets received by PC: 31
missing packets: 39
overall device -> PC loss: 55.7%
rejected locally by ESP32 sendto(): 0
accepted by ESP32 network stack: 70
loss after local socket acceptance: 55.7%

=== PC -> device (ESP32 receive path) ===
PC send attempts: 601
PC sendto() successes: 601
PC sendto() errors: 0
device received after baseline: 598
PC -> device missing packets: 3
PC -> device loss: 0.5%

Example result 2

=== device -> PC (ESP32 transmit path) ===
sequence span: 10..79 (70 attempts)
unique packets received by PC: 27
missing packets: 43
overall device -> PC loss: 61.4%
rejected locally by ESP32 sendto(): 0
accepted by ESP32 network stack: 70
loss after local socket acceptance: 61.4%

=== PC -> device (ESP32 receive path) ===
PC send attempts: 601
PC sendto() successes: 601
PC sendto() errors: 0
device received after baseline: 601
PC -> device missing packets: 0
PC -> device loss: 0.0%

Repeated 60-second results

Test ESP32-S3 to PC loss PC to ESP32-S3 loss
1 55.7% 0.5%
2 61.4% 0.0%
3 78.0% 18.3%
4 36.2% 0.0%
5 70.6% 0.0%
6 63.6% 0.0%
7 77.3% 0.0%
8 67.6% 0.3%
9 54.4% 5.2%
10 67.2% 0.2%
11 53.1% 1.2%
12 64.3% 0.0%
13 63.8% 0.0%

The ESP32-S3 transmit direction is consistently much worse than its receive direction.

600-second test

I also ran a longer test to rule out short-test timing effects.

=== device -> PC (ESP32 transmit path) ===
sequence span: 469..1076 (608 attempts)
unique packets received by PC: 238
missing packets: 370
overall device -> PC loss: 60.9%
rejected locally by ESP32 sendto(): 0
accepted by ESP32 network stack: 608
loss after local socket acceptance: 60.9%

=== PC -> device (ESP32 receive path) ===
PC send attempts: 6001
PC sendto() successes: 6001
PC sendto() errors: 0
device received after baseline: 5773
PC -> device missing packets: 228
PC -> device loss: 3.8%

The long test confirms that the problem is persistent.

XIAO ESP32-C6 control test

I ran the same bidirectional UDP test with a Seeed Studio XIAO ESP32-C6.

=== device -> PC (ESP32 transmit path) ===
sequence span: 66..135 (70 attempts)
unique packets received by PC: 70
missing packets: 0
overall device -> PC loss: 0.0%
rejected locally by ESP32 sendto(): 0
accepted by ESP32 network stack: 70
loss after local socket acceptance: 0.0%

=== PC -> device (ESP32 receive path) ===
PC send attempts: 601
PC sendto() successes: 601
PC sendto() errors: 0
device received after baseline: 601
PC -> device missing packets: 0
PC -> device loss: 0.0%

The C6 result was:

  • XIAO ESP32-C6 to PC: 0.0% loss
  • PC to XIAO ESP32-C6: 0.0% loss
  • Local UDP send failures: 0

This control test indicates that:

  • the Python measurement script works;
  • the Linux PC can receive UDP correctly;
  • the PC firewall is not generally blocking this traffic;
  • the packet rate is not too high;
  • the problem appears to be specific to the XIAO ESP32-S3, its external antenna/RF path, or its Wi-Fi configuration.

Important observation about local send failures

The ESP32-S3 usually reports zero local UDP send failures.

This means that NetworkUDP.endPacket() or sendto() accepted the packet locally. I understand that this does not guarantee successful over-the-air delivery.

The loss could therefore occur in:

  • the ESP32-S3 Wi-Fi driver;
  • the RF transmit path;
  • the external antenna or U.FL/I-PEX connector;
  • 802.11 transmission and retries;
  • access-point forwarding.

Because the PC-to-ESP32 direction is usually almost lossless, the problem appears to be mainly associated with the ESP32-S3 transmit direction.

Troubleshooting already performed

  • Disabled Wi-Fi sleep with WiFi.setSleep(false)
  • Used very small UDP packets
  • Reduced the ESP32-S3 transmission rate to one packet per second
  • Added sequence numbers in both directions
  • Added a 10-second drain period
  • Recorded local UDP send failures
  • Repeated the test many times
  • Ran a 600-second test
  • Tested a XIAO ESP32-C6 as a control
  • Confirmed that the C6 has 0% loss using the same test method
  • Used the Arduino WiFi and NetworkUDP APIs
  • Confirmed that the external antenna is connected

Questions

  1. Is severe asymmetric packet loss a known issue on some XIAO ESP32-S3 boards?
  2. Has Seeed reproduced cases where the S3 receives normally but transmits very poorly?
  3. Is there a recommended transmit-power setting for the XIAO ESP32-S3?
  4. Should I test lower power settings such as 8.5, 11, 13, or 15 dBm?
  5. Can a damaged, poorly connected, or poorly matched antenna cause good reception but severe transmission loss?
  6. Are there specific Arduino-ESP32 Core versions recommended for this board?
  7. Are there known production batches with RF or antenna-connector problems?
  8. Would these results be sufficient to consider the board defective and request a replacement?

I can provide:

  • the complete Arduino sketch;
  • the Python UDP test script;
  • complete test logs;
  • RSSI and channel information;
  • photographs of the antenna connector;
  • results from multiple antennas or additional XIAO ESP32-S3 boards.

Thank you in advance.

Andy

Hi there,

So from a 5oooo’ view I would say that you have a code issue with the ANTENNA RF-switch logic somewhere. The transmit side would indeed be greatly effected, the C6 does not have that same setup,
can you post the code, and have you done any s3 to s3 test to see if they agree with each other at all?
What kind of AP are you using is it a wifi-6 compatible?
Which BSP file are you building with, (my first guess) :backhand_index_pointing_left: :grin:
Only because it’s just the S3’s that are having issue and they are the SR. devices.
Post a picture please. Let’s get a LQQK at this setup :+1:

Stick with it , you figure it out. With those numbers you should be able to get to the bottom of it. Lots at play though. :crossed_fingers:

HTH
G:L :slight_smile: PJ :v:

Hello,

Thank you for your reply and suggestions. I have been struggling with this issue for the past two days, but I have found the trigger causing this abnormal behavior, i.e. channel occupancy.

Short version: the loss depends on how busy the channel is, not on the board.
Moving my S3s off channel 6 took transmit loss from ~60% to 0.0%.

What I measured

Channel 6 at my location carries ~500–720 frames/second (two APs plus all
their clients). Channel 1 carries ~50.

I put an ESP32 in promiscuous mode to count frames/second on each channel, then
ran the same UDP test on each:

Channel Occupancy S3 transmit loss
6 500–720 f/s 46–74%
1, 2, 3, 4, 5, 7, 8, 9, 10, 12, 13 9–80 f/s 0%
11 (first sweep) 77 f/s 0%
11 (second sweep) 105 f/s and rising 49%

Two things worth noting:

  • Channels 5 and 7 are directly adjacent to 6 and sit in the same spectrum,
    but they are clean. So it is traffic, not frequency.
  • Channel 11 was clean at 77 f/s and failed at 105 f/s in the next sweep when
    a neighbour got busy. Same board, different frequency, failing purely because
    traffic arrived.

After moving my ESP32-S3 boards to channel 1 I get 0.0% loss over UDP, over
ESP-NOW, and on a persistent TCP connection running 158 s with zero reconnects.

Answering the earlier suggestions

  • Antenna / RF-switch: I ran the test with the antenna completely removed
    — a ~34 dB change. Transmit loss moved by 2 points. It is not the antenna.
  • Arduino-ESP32 BSP version: tested 2.0.8, 2.0.17, 3.1.0 and 3.3.11 — that is
    ESP-IDF 4.4.4 through 5.5.5, about four years of driver development. All
    identical, ~60–70%.
  • Wi-Fi 6 AP compatibility: reproduced with ESP-NOW, which has no access
    point, no association, no DHCP and no TCP. Two boards one metre apart, same
    ~60% loss. The AP is not involved.

Other things that made no difference

  • TX power 8.5 / 10 / 15 / 20 dBm, verified applied with esp_wifi_get_max_tx_power()
  • Power supply, USB cable, and a power-only cable with the board away from the PC
  • 802.11 b/g forced (protocol bitmap verified 0x03), 20 MHz channel width
  • Payload 4–250 bytes
  • PHY rate 1 Mbps DSSS through 24 Mbps OFDM — the fastest rate performed best

What actually happens to the lost frames

I ran three boards at once: sender, receiver, and an independent ESP32 in
promiscuous mode as an air sniffer, correlating every sequence number across all
three. Out of 289 failed sends:

  • frames seen on air: 0 / 289
  • 802.11 Retry bit seen: 0 / 289
  • successful sends the same sniffer captured: 211 / 211

The sniffer caught every successful frame, so the missing ones are not a capture
miss. They are never transmitted.

esp_now_send() returns ESP_OK, there are no immediate rejects, and the frame
is then dropped unsent after a fixed timeout. The failure latency is a hard
constant — across 289 samples: min 1,047,203 µs, mean 1,047,707 µs, max
1,048,212 µs. That is a 1 ms spread, sitting just under 2²⁰ µs = 1.048576 s.

Comparison with another chip on the same channel

Running the same test simultaneously on a classic ESP32 on channel 6:

classic ESP32 XIAO ESP32-S3
success rate 900 / 900 ~42%
mean time from send to callback 87 ms 437 ms (successes only)
worst case 818 ms hits the 1.048 s timeout

On a quiet channel the two chips are indistinguishable — both 900/900, mean
1.7 ms. Congestion inflates both, but the classic ESP32’s worst frame still gets
out at 818 ms, just inside the deadline. The S3 needs longer than 1.048 s for
about 58% of frames.

So, practically

If you are on a busy 2.4 GHz channel, an ESP32-S3 will lose most of what it
tries to send while other chips on the same channel are fine. Measure the
occupancy on your channel, then move to one at least 5 channels away from the
busiest one and re-test.

What I could not determine from outside the chip is why the S3’s scheduler
cannot find a transmit opportunity in a full second where other chips manage it
in under one. esp_wifi_get_tx_statistics() would answer this directly, but it
is declared in esp_wifi_he.h and so is only available on Wi-Fi 6 parts, not
the S3.

Happy to share the test sketches (UDP probe, ESP-NOW probe, air-correlation
probe and the analysis script) if anyone wants to reproduce this.

Hi there,

And nice work , bringing the receipts.
:grin:
WOW, so I saw this crowded channel phenom , before but between AP and a particular Microwave oven back in the day of early wifi. But never here on the Micro’s

Seems it’s a thing on Espresiff hardware, Go figiure.
only the S3, Our LLM gave me straight Espresso Grind; :+1:

"He really did bring the receipts—that methodology (air sniffer correlation, packet timing, sweep testing) is top-tier debugging! :backhand_index_pointing_left: :wink:

What was uncovered is a classic Clear Channel Assessment (CCA) / Medium Access Control (MAC) backoff issue, specifically tied to how the ESP32-S3’s Wi-Fi MAC layer handles contention compared to the older ESP32 or the newer C6.

Here is what is likely happening under the hood on the ESP32-S3 to cause this:

  • Overly Aggressive / Flawed CSMA/CA Backoff: Wi-Fi devices use Carrier Sense Multiple Access with Collision Avoidance (CSMA/CA). Before transmitting, the chip listens to the air. If the channel is busy (like the 500–720 frames/sec on channel 6), it picks a random backoff timer and waits. The S3’s Wi-Fi stack seems to have an overly conservative backoff curve or a flawed retry scheduler that gets repeatedly preempted on high-occupancy channels.

  • Hardcoded ~1-Second Driver Timeout: As the sniffer log proved, the S3 isn’t actually dropping packets on the air; it’s dropping them before they ever hit the RF power amplifier. The Espressif Wi-Fi driver on the S3 puts incoming frames into an internal TX queue with a hard limit of roughly 2^20us(~ 1.048seconds. If the MAC layer can’t find a clean transmit slot before that 1.048s deadline expires, the driver quietly discards the buffer and reports success or timeout internally.

  • Difference in Hardware Wi-Fi MAC Cores: The original ESP32 uses a mature, highly battle-tested Tensilica Wi-Fi MAC controller that handles contention aggressively enough to push packets out within 800ms. The ESP32-S3 introduced a revamped MAC/PHY IP block. The C6, on the other hand, runs a Wi-Fi 6 (802.11ax) core with Target Wake Time (TWT) and OFDMA support, which inherently handles crowded spectrum far better.

It’s rarely brought up on the forum because most people assume high packet loss is caused by poor antennas, low power, or weak AP signals. His test removing the antenna entirely completely blew those usual theories out of the water! :+1:

I ask , more…but truth is they suc at it. :face_with_peeking_eye:

Here is why a software or BSP fix hasn’t surfaced and what the actual workaround is:

Why a BSP Fix Doesn’t Exist

  • Hardcoded Hardware/Driver Timeout: The core issue is that the ESP32-S3’s Wi-Fi MAC layer drops packets internally after a hardcoded timeout of 2^20us ~(1.048 seconds). When channel occupancy is high (like 500–720 frames/sec), the S3’s MAC backoff algorithm consistently fails to claim an air slot before that 1-second timer expires.

  • Pre-Transmission Drop: Because esp_now_send() or the UDP stack returns ESP_OK before silently dropping the frame off the queue, higher-level software layers are completely unaware the frame failed due to RF contention.

  • Silicon Architecture Limits: The classic ESP32 handles MAC contention inside 818 ms under the same load, while the ESP32-C6 handles it natively via Wi-Fi 6 MAC hardware. The S3 sits on an intermediate Wi-Fi IP block where the contention scheduler simply stalls under heavy 2.4 GHz traffic.


The Only Practical Solutions Today

  1. Channel Management (Immediate Fix): Move the Wi-Fi AP or ESP-NOW link to a low-occupancy channel at least 5 channels away from heavy traffic. Dropping channel occupancy from ~500 f/s to <80 f/s instantly took the S3’s packet loss from 46–74% down to 0.0%.

  2. Switch Microcontrollers for Heavy 2.4 GHz Environments: If operating on congested channels is mandatory, switching to the XIAO ESP32-C6 (or Nordic nRF series) completely eliminates the MAC backoff queue issue due to their superior Wi-Fi 6 / radio hardware schedulers.

I would move to the C5 or 6, or wait on a WIFI add-on XIAO board for the Nrf54LM20A , or go with the tech you even found out on your own , move some stuff around the Channel spectrum… doesn’t seem very future proof that way I would Agree! :crossed_fingers:

GREAT TOPIC bro, nice contribush’ :grin:

GL :slight_smile: PJ :v: