Skip to content

Performance and Timing

zackees edited this page Aug 2, 2026 · 1 revision

Performance and Timing

(Or, how long does FastLED.show() actually take, and will it break my other code?)

Interrupt problems explains what goes wrong when show() collides with time-critical work. This page is the other half: how to predict the numbers up front, so you can evaluate a microcontroller for a project before you build it.

The good news is that clockless LED timing is not mysterious or measured — it is fixed by the chipset protocol and is computable to the microsecond. You do not need to profile it. You need one formula and one table.


The formula

For any clockless (3-wire) chipset:

show() duration ≈ (bits_per_pixel × bit_period × num_pixels) + reset_time
  • bits_per_pixel is 24 for RGB chipsets, 32 for RGBW (SK6812/WS2814/TM1814).
  • bit_period is fixed by the chipset — every bit takes the same time, whether it is a 0 or a 1. That is how the protocol encodes data: the ratio of high-to-low varies, the period does not.
  • reset_time is the low period that latches the frame. It is paid once per show(), not per pixel.

Worked example — the classic case:

100 WS2812 pixels: 24 × 1.25µs × 100 = 3000µs of data, plus 280µs reset ≈ 3.3 ms.

That is the number to plan around. It is not affected by brightness, by colour, by how much your effect code does, or by which platform you are on. It is the wire protocol.


Per-pixel cost by chipset

All values below are computed from FastLED's own timing table (src/fl/chipsets/led_timing.h), which is the same data the drivers compile against. bit period = T1 + T2 + T3.

ChipsetBit periodPer pixel (24-bit)Per pixel (32-bit RGBW)Reset
WS2812 / WS2812B1250 ns30.00 µs280 µs
WS2812B-V51225 ns29.40 µs280 µs
WS28131280 ns30.72 µs300 µs
WS2814 (RGBW)1280 ns40.96 µs300 µs
WS28151890 ns45.36 µs0
SK68121200 ns28.80 µs38.40 µs80 µs
SK68221750 ns42.00 µs0
GS19031200 ns28.80 µs280 µs
SM167031200 ns28.80 µs0
SM16824E1200 ns28.80 µs200 µs
TM18091150 ns27.60 µs0
TM1814 (RGBW)1300 ns41.60 µs300 µs
TM1829 @ 800 kHz1230 ns29.52 µs500 µs
TM1829 @ 1600 kHz600 ns14.40 µs500 µs
PL98231710 ns41.04 µs0
GE88221360 ns32.64 µs0
LPD1886 @ 1250 kHz800 ns19.20 µs0
UCS1903B @ 800 kHz1300 ns31.20 µs0
UCS1904 @ 800 kHz1250 ns30.00 µs0
UCS29031250 ns30.00 µs0
UCS19121600 ns38.40 µs0
UCS7604 @ 800 kHz1250 ns30.00 µs280 µs
UCS7604 @ 1600 kHz625 ns15.00 µs280 µs
WS2811 @ 400 kHz2500 ns60.00 µs280 µs
UCS1903 @ 400 kHz2500 ns60.00 µs0
TM1803 @ 400 kHz2500 ns60.00 µs0
GW6205 @ 400 kHz2400 ns57.60 µs0
GW6205 @ 800 kHz1200 ns28.80 µs0
DP1903 @ 400 kHz3200 ns76.80 µs0
DP1903 @ 800 kHz1800 ns43.20 µs0

Two things worth noticing:

  • 400 kHz parts cost double. If you have a choice between a WS2811 strip at 400 kHz and an 800 kHz part, that is a 2× difference in blackout time for the same pixel count.
  • RGBW costs 33% more than RGB on the same chipset, because it is 32 bits per pixel instead of 24.

Maximum frame rate

Frame rate is bounded by 1 / show() duration, before your effect code costs anything:

Pixels (WS2812)show()Ceiling
80.52 ms~1900 fps
501.78 ms~560 fps
1003.28 ms~305 fps
3009.28 ms~108 fps
50015.28 ms~65 fps
100030.28 ms~33 fps
200060.28 ms~17 fps

At around 1000 pixels on a single data line you are down to 33 fps and the protocol — not your microcontroller — is the bottleneck. No amount of CPU speed changes this. The fix is parallel output: driving N strips simultaneously divides the wall-clock time by N. See Parallel-Output.

FastLED also enforces a per-chipset getMaxRefreshRate(), so calling show() faster than the chipset can latch will simply block until it is safe.


The part that breaks other code: interrupt blackout

For clockless chipsets, the bit timing is enforced in software, so interrupts must be held off while data is going out — that is the whole of the Interrupt problems story. What varies is how much is held off, and that is a platform property.

FastLED's default per platform (from each led_sysdefs_*.h):

PlatformFASTLED_ALLOW_INTERRUPTS defaultWhat it means
AVR (Uno, Nano, Mega, Duemilanove…)0Interrupts off for the entire frame
STM320Interrupts off for the entire frame
Teensy 3.x / 4.x, SAMD21/51, nRF51/52, RP2040/RP2350, SAM, Apollo3, Renesas, Giga, MGM2401Re-enabled briefly between pixels
ESP82661Re-enabled between pixels, with frame retry
ESP321See below — usually not applicable

This is the single most important number for evaluating a microcontroller.

On an AVR, a 100-pixel WS2812 frame means 3.3 ms with interrupts completely off. Any interrupt-driven peripheral — hardware serial, I²C, Servo, IR receive, millis() accuracy — is stalled for that entire window. This is exactly what @afaucher hit polling IR data on a Duemilanove.

On platforms with FASTLED_ALLOW_INTERRUPTS 1, interrupts get a window between each pixel. The constraint becomes: your ISR must complete in well under the per-pixel budget (a few µs), and must not need to fire more often than once per pixel period (~30 µs for WS2812). If it overruns, FastLED detects the corrupted frame and retries it — see FASTLED_INTERRUPT_RETRY_COUNT.

On ESP32, the default clockless path uses the RMT peripheral with DMA. The peripheral clocks the waveform out in hardware, so show() does not need to hold interrupts off for the frame at all. This is why an ESP32 can drive long strips alongside WiFi.

Chipsets that avoid the problem entirely

Clocked (4-wire) chipsets — APA102, SK9822, DotStar, LPD8806, WS2801 — have no timing requirement at all. The clock line carries the timing, so an interrupt mid-frame is harmless; the transfer just pauses. FastLED never disables interrupts for these.

Their duration is set by SPI clock instead: 32 bits × num_pixels / SPI_clock. At DATA_RATE_MHZ(12), 100 APA102 pixels take ~270 µs — more than 10× faster than WS2812, with zero interrupt cost.

If you have time-critical work and freedom to choose parts, this is the highest-leverage decision available to you.


Evaluating a microcontroller for your project

A procedure you can run on paper before ordering anything:

  1. Compute your frame cost.bits × bit_period × pixels + reset, from the table above.
  2. Find your tightest interrupt deadline. For hardware serial at 57600 baud on AVR that is ~174 µs (one byte time, single-byte receive buffer). For a 38 kHz IR receiver it is tens of µs. For I²C it is set by the master's clock.
  3. Compare them.
    • If frame cost < deadline: fine on any platform.
    • If frame cost > deadline and your platform defaults to FASTLED_ALLOW_INTERRUPTS 0 (AVR, STM32): you will lose data. At 57600 baud on AVR that threshold is about 5 WS2812 pixels.
    • If frame cost > deadline but the platform allows interrupts: check your ISR duration against the per-pixel window instead of the whole frame.
  4. Check your frame-rate ceiling against the table above.
  5. Check RAM. 3 bytes per pixel for the CRGB array (4 for RGBW), plus your framebuffer if you keep one. On a 2 KB AVR that caps you near a few hundred pixels regardless of timing.

If it does not fit

Roughly in order of effectiveness:

  1. Switch to a clocked chipset (APA102/SK9822). Removes the interrupt problem entirely and is ~10× faster.
  2. Move to a platform with a hardware LED peripheral — ESP32 (RMT/I2S/LCD_CAM/PARLIO), RP2040/RP2350 (PIO), Teensy 4 (FlexIO/ObjectFLED). These clock the waveform out in hardware.
  3. Use parallel output. N strips at once divides wall-clock by N. See Parallel-Output.
  4. Split into shorter strips on separate controllers — shorter individual blackout windows.
  5. Call show() less often. @afaucher's own fix: update on state change rather than every loop iteration. Often the cheapest change available.
  6. Prefer an 800 kHz part over a 400 kHz one — an immediate 2× win.
  7. Move time-critical work off interrupts, or onto a second core (ESP32, RP2040).

Measuring it yourself

The formula predicts the wire time. If you want to measure the whole loop including your effect code, time it directly:

voidloop() {
fl::u32 t0 = micros();
myEffect();
fl::u32 t1 = micros();
FastLED.show();
fl::u32 t2 = micros();
Serial.print("effect: "); Serial.print(t1 - t0);
Serial.print("us show: "); Serial.println(t2 - t1);
}

Note that on AVR, micros() itself depends on a timer interrupt — so a show() that disables interrupts will make micros()lose time, not gain it. Measure across the whole loop, and treat the show() figure as a lower bound. On platforms that permit interrupts during show(), the reading is accurate.

FastLED.getFPS() reports the achieved frame rate directly, which is usually the number you actually care about.


See also

Clone this wiki locally