Library / SDK
arkhipenko/TaskScheduler avatar
arkhipenko/TaskScheduler

TaskScheduler 4.1.0 adds stale-request protection and changes what Task::cancel() does

Cooperative multitasking for Arduino, ESPx, STM32, nRF and other microcontrollers

1,658 stars293 forksC++BSD-3-Clause

At a glance

What is it?
TaskScheduler is a cooperative multitasking library for Arduino, ESP8266, ESP32, STM32 and nRF boards that runs your tasks from a single loop() call. Version 4.1.0, dated 2026-09-26, ships a new Scheduler destructor plus several behaviour changes that can break code written against 4.0.x.
Who is it for?
TaskScheduler fits firmware that needs several periodic or event-driven jobs on one microcontroller without the weight of FreeRTOS, especially on ESP and STM32 targets where std::function callbacks and tickless operation under FreeRTOS are listed as tested. It does not fit code that needs preemption or real isolation between tasks, since one task that overruns its period delays everything behind it.
Can I use it commercially?
Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One loop() call, many callbacks, no RTOS

The library gives you a global Scheduler object, tasks bound to it, and a single call to `runner.execute()` inside `loop()`. The smallest useful sketch from the README does exactly that:

cpp
#include <TaskScheduler.h>

Scheduler runner;
Task taskBlink(500, TASK_FOREVER, &blinkLED);
Task taskSensor(2000, TASK_FOREVER, &readSensor);

void setup() {
    runner.addTask(taskBlink);
    runner.addTask(taskSensor);
    taskBlink.enable();
    taskSensor.enable();
}

void loop() {
    runner.execute();
}

The first Task constructor argument is the period, the second is the iteration count, and TASK_FOREVER means the task runs indefinitely. A third style exists where the callback takes no arguments and returns a bool, which is what the OO callback examples use.

The pitch is not that preemptive scheduling is wrong, but that a loop-driven design removes whole categories of bugs. The README quotes Herb Sutter on people who think they understand concurrency, and argues that cooperative processing avoids races, deadlocks and livelocks by construction. That argument is sound for the shape of problem the library targets, and it is also the library's limit: nothing preempts a long callback, so a task that runs past its period delays the ones behind it. That behaviour is visible in the feature list, which describes priority scheduling with or without catchup rather than any preemption.

Cost of a scheduling pass, and where the library does not fit

The README states scheduling overhead as between 15 and 18 microseconds per scheduling pass, measured on an Arduino UNO rev 3 at a 16MHz clock with a single scheduler and no prioritisation. That figure is a real constraint on 8-bit parts with long task lists, and it is also the number most people should re-measure on their own board before committing, since it is quoted for one configuration only.

The tested platform list is longer than the name suggests. Beyond Arduino Uno R3, Nano and Micro and ATtiny85, it covers ESP8266 and ESP32, Teensy (tested on Teensy 3.5), nRF52832 and nRF52 Adafruit Core (tested on nRF52840 with a v3.6.2 workaround), STM32 (tested on a leaflabs Leaf maple mini module F), MSP430 and MSP432. Raspberry Pi and any Linux board work only with `_TASK_NON_ARDUINO` defined and your own `_task_millis()` implementation, with `_task_micros()` and `_task_yield()` where the options you enable need them. The library does not ship that port for you; the note points at the unit test tests/test-scheduler-defect-fixes.cpp as a working example, and insists the functions live in the same source file that includes TaskScheduler.h.

Where it is the wrong tool: firmware with hard real-time deadlines, anything needing per-task stack or priority isolation, or code where a blocking delay inside a callback must not delay neighbours. Cooperative means your callback discipline is the scheduler.

Installing through the library manager or platformio.ini

Installation has three documented paths. In the Arduino IDE it is Sketch, then Include Library, then Manage Libraries, then search for TaskScheduler and install. For PlatformIO, one line in platformio.ini is enough:

ini
lib_deps = arkhipenko/TaskScheduler

For a manual install, download or clone the repository into your Arduino libraries folder. The repository supports both library layouts: Arduino IDE style with headers only, and PlatformIO style with a header plus CPP files.

Before writing hardware code there is a simulator. The README links a Wokwi playground sketch for TaskScheduler, so the quick start example can be exercised without a board. There is also a unit test suite run in CI, referenced by the test badge, which matters given the density of version 4.1.0's fixes.

setIntervalNodelay() behaves differently in 4.1.0 than it did

This is the change most likely to cost you an afternoon. In 4.1.0 the setIntervalNodelay() options work as documented. TASK_INTERVAL_KEEP, the default, keeps the already scheduled next run. TASK_INTERVAL_RESET restarts the schedule from now. And a task that changes its own interval with the default option now runs once more at the old interval before the new one takes effect; if you do not want that, use TASK_INTERVAL_RECALC to apply the new interval right away.

The same release fixes a related trap: `setIntervalNodelay(..., TASK_INTERVAL_RECALC)` no longer stalls a task for about 49.7 days when the interval shrinks. A quarter-century stall from an unsigned arithmetic bug is exactly the class of failure that ships in devices and is found in the field.

Two structural changes come with the same version. Scheduler gained a destructor, and Scheduler objects can no longer be copied, so they must be passed by reference or pointer; code that stored a Scheduler by value stops compiling, which is the intended outcome since copying a scheduler with tasks attached has never meant anything. On the thread-safe side, the `_task_request_t` structure gained a `generation` field and stale requests are now detected, which means any queue code of yours that copies that structure needs recompiling against the new layout.

Task::cancel() no longer aborts the tasks waiting on it

The other behaviour change worth grepping for before an upgrade is `Task::cancel()`. It used to complete the task's internal StatusRequest with TASK_SR_ABORT; in 4.1.0 it completes it with TASK_SR_CANCEL. The consequence is concrete: a task waiting on a canceled task now runs, where it previously would have been aborted.

If your firmware treats a canceled task as a failure that must abort its dependents, this silently inverts your error handling. That is the single line to check across a codebase.

Related fixes in the same release: a task can now delete itself inside its own OnDisable() method, and destroying a scheduler before its tasks no longer leaves those tasks pointing at freed memory. With layered priority and `_TASK_SLEEP_ON_IDLE_RUN`, the base scheduler now actually sleeps, which examples 11 and 12 previously did not do, so measured power consumption on those examples will change. Tickless getNextRun() also now works with `_TASK_TIMEOUT` and across a millis() rollover, and the event-only Task constructor initializes the self-destruct flag.

Sleep, timeouts and the cooperative power story

Power behaviour is a headline feature rather than an afterthought. Feature 7 is IDLE sleep mode when no task is scheduled to run, which on Arduino-family boards means the MCU spends the gap in its idle state instead of spinning through execute(). Feature 8 is an overall task timeout, so a task that overruns can be detected rather than silently stalling the schedule. Feature 9 gives CPU load and idle statistics for time-critical work, and the TimeCritical example shows how.

Feature 11 is the interesting one for anyone already running FreeRTOS: tickless execution means the scheduler sleeps continuously until the next scheduled invocation rather than waking on a timer. That is where the library stops being an alternative to an RTOS and starts being a component inside one, which is also the context for the thread-safe scheduling option in feature 17.

Layered prioritisation sits on top. The two priority examples exist to show it, and the fix in 4.1.0 that made the base scheduler honour `_TASK_SLEEP_ON_IDLE_RUN` under layered priority suggests those two features had not been combined properly before.

Event-driven work through StatusRequest and OO callbacks

Periodic scheduling is only half the API. Feature 12 is event-driven task invocation through a Status Request object: a task parks waiting for a request, and something in the system hands it one. Examples 04, 05 and 20 cover the pattern, and feature 14 adds a Local Task Storage pointer so one callback body can serve several tasks with different context.

Feature 13 bundles task IDs and Control Points, which together are how you hook into error handling and an external watchdog timer. Example 07 covers the watchdog integration and example 08 covers Local Task Storage, with example 18 combining StatusRequest, LTS, watchdog and timeout in one object-oriented sketch. Example 21 covers OO callbacks, and feature 19 notes static and dynamic callback binding plus support for std::function, which the README says is tested on ESPx and STM32.

Two behaviour notes here. Under `_TASK_OO_CALLBACKS`, a scheduling pass counts as idle only if no callback returned true, and getInvokedTasks() now counts OO callbacks, so idle statistics will read differently after upgrading. Under `_TASK_THREAD_SAFE`, the stale-request protection mentioned above applies. Dynamic creation is covered by example 19, including a self-destruct variant that destroys a task once it is disabled.

Editorial conclusion

TaskScheduler fits firmware that needs several periodic or event-driven jobs on one microcontroller without the weight of FreeRTOS, especially on ESP and STM32 targets where std::function callbacks and tickless operation under FreeRTOS are listed as tested. It does not fit code that needs preemption or real isolation between tasks, since one task that overruns its period delays everything behind it. Before upgrading to 4.1.0, audit every caller of Task::cancel() and every setIntervalNodelay() call, because waiting tasks now run rather than abort, and Scheduler objects can no longer be copied.

Frequently asked questions

How do I install TaskScheduler in PlatformIO?

Add `lib_deps = arkhipenko/TaskScheduler` to platformio.ini. In the Arduino IDE, use Sketch, Include Library, Manage Libraries and search for TaskScheduler. A manual install means cloning the repository into your libraries folder.

What changed in TaskScheduler 4.1.0?

Version 4.1.0 dated 2026-09-26 adds Scheduler::isEnabled(), Scheduler::getState(), a Scheduler destructor and stale-request protection in _TASK_THREAD_SAFE queues. Task::cancel() now completes with TASK_SR_CANCEL instead of TASK_SR_ABORT, and Scheduler objects can no longer be copied.

How do I use TaskScheduler on a Raspberry Pi or Linux board?

Define _TASK_NON_ARDUINO and implement _task_millis() yourself, adding _task_micros() and _task_yield() where your options need them. The functions must be defined in the same source file that includes TaskScheduler.h; the unit test tests/test-scheduler-defect-fixes.cpp demonstrates a working setup.

What is the scheduling overhead of TaskScheduler?

The README quotes 15 to 18 microseconds per scheduling pass on an Arduino UNO rev 3 at 16MHz with a single scheduler and no prioritisation. That is the only configuration the number covers.

Can I try TaskScheduler without hardware?

Yes. The README links a Wokwi playground sketch for TaskScheduler that runs the quick start example in the browser, and the project runs unit tests in CI through GitHub Actions.

Which platforms is TaskScheduler tested on?

Arduino Uno R3, Nano and Micro, ATtiny85, ESP8266 and ESP32, Teensy (tested on Teensy 3.5), nRF52832 and nRF52 Adafruit Core (tested on nRF52840 with a v3.6.2 workaround), STM32 (tested on a leaflabs Leaf maple mini F), and MSP430 and MSP432 boards.

Official sources

  1. arkhipenko/TaskScheduler on GitHub
  2. Issues
  3. License: BSD-3-Clause
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/arkhipenko-taskscheduler.svg)](https://hysenlabs.com/projects/arkhipenko-taskscheduler)

Was this useful?