The Core Definition: What is an Unsigned Long?
In the Arduino C++ ecosystem, the unsigned long data type is a 32-bit unsigned integer. It occupies 4 bytes (32 bits) of memory and can store values ranging from 0 to 4,294,967,295. Because it lacks a sign bit, it cannot represent negative numbers, but it doubles the maximum positive capacity of a standard signed 32-bit long.
This data type is the backbone of timekeeping in embedded systems. Functions like millis() and micros() return unsigned long values, making a deep understanding of its limits, overflow behaviors, and architectural quirks mandatory for any firmware engineer working with AVR, ARM, or RISC-V microcontrollers in 2026.
Data Type Matrix: Memory and Limits
Before diving into edge cases, it is critical to understand how unsigned long compares to other common integer types across standard 8-bit (AVR) and 32-bit (ARM/RISC-V) architectures.
| Data Type | Size (AVR / 8-bit) | Size (ARM / 32-bit) | Minimum Value | Maximum Value |
|---|---|---|---|---|
int |
2 bytes (16-bit) | 4 bytes (32-bit) | -32,768 / -2,147,483,648 | 32,767 / 2,147,483,647 |
unsigned int |
2 bytes (16-bit) | 4 bytes (32-bit) | 0 | 65,535 / 4,294,967,295 |
long |
4 bytes (32-bit) | 4 bytes (32-bit) | -2,147,483,648 | 2,147,483,647 |
unsigned long |
4 bytes (32-bit) | 4 bytes (32-bit) | 0 | 4,294,967,295 |
uint32_t |
4 bytes (32-bit) | 4 bytes (32-bit) | 0 | 4,294,967,295 |
Expert Note: Notice that while the size of a standardintchanges depending on whether you are compiling for an ATmega328P (Arduino Uno) or an RP2040 (Raspberry Pi Pico), theunsigned longremains strictly 32-bit across both architectures. This makes it highly portable for timing operations.
The 49.7-Day Rollover: Handling millis() Overflow
The most notorious challenge associated with the unsigned long data type is the millis() rollover. Because millis() increments every millisecond, it will eventually hit its maximum value of 4,294,967,295. At exactly 49.71 days of continuous uptime, the variable overflows and wraps around to 0.
If your code uses addition to check for timeouts, your system will lock up or behave erratically at the 49.7-day mark.
The Wrong Way (Addition)
unsigned long previousMillis = 0;
unsigned long interval = 1000;
void loop() {
unsigned long currentMillis = millis();
// BUG: currentMillis + interval will overflow and fail during rollover
if (currentMillis > previousMillis + interval) {
previousMillis = currentMillis;
// Toggle LED
}
}
The Right Way (Subtraction)
According to the official Arduino Language Reference, you must always use subtraction to handle unsigned math wrap-around.
void loop() {
unsigned long currentMillis = millis();
// SAFE: Unsigned subtraction naturally handles the 32-bit wrap-around
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
// Toggle LED
}
}
Why this works: Unsigned integers in C++ obey the rules of modular arithmetic. If currentMillis has rolled over to 5, and previousMillis was 4,294,967,290, the subtraction 5 - 4,294,967,290 mathematically underflows and wraps back around to exactly 11. The logic holds perfectly without requiring complex if (currentMillis < previousMillis) reset statements. For a deep mathematical breakdown of this behavior, Nick Gammon's authoritative guide on millis() overflow remains the gold standard for embedded engineers.
The Hidden Trap: Integer Promotion and the 'UL' Suffix
A frequent source of bugs in Arduino sketches occurs when calculating large time intervals using literal numbers. Consider a scenario where you want a 1-hour delay (3,600,000 milliseconds).
unsigned long oneHour = 1000 * 60 * 60; // DANGER
On 8-bit AVR boards (like the Uno or Nano), the compiler treats raw numerical literals as 16-bit signed int types by default. While 1000 * 60 equals 60,000 (which fits in a 16-bit int), multiplying that result by 60 yields 3,600,000. This vastly exceeds the 16-bit signed maximum of 32,767. The compiler silently overflows the intermediate calculation, resulting in a corrupted, negative number that is then cast into your unsigned long variable, causing immediate, erratic timing failures.
The Fix: Force Unsigned Long Literals
You must explicitly tell the compiler to treat the first literal as an unsigned long by appending the UL suffix. This forces the entire chain of multiplication to promote to 32-bit unsigned math.
unsigned long oneHour = 1000UL * 60 * 60; // CORRECT
Always use the UL suffix on at least the first operand in any timing calculation that exceeds 32,767. Alternatively, modern C++ best practices recommend using constexpr with explicit typing to catch these errors at compile time.
Modern C++ Alternatives: unsigned long vs uint32_t
While unsigned long is deeply ingrained in legacy Arduino documentation, modern firmware development in 2026 heavily favors the <stdint.h> library. As noted by C++ Reference standards, using explicit-width integers eliminates architectural ambiguity.
unsigned long: Guaranteed to be at least 32 bits. On some obscure 64-bit embedded Linux SBCs, it could theoretically be 64 bits.uint32_t: Guaranteed to be exactly 32 bits on any platform that supports it.
Recommendation: If you are writing a library or code that must remain strictly portable across AVR, ESP32-S3, and Renesas UNO R4 boards, include #include <stdint.h> and use uint32_t for all timing and large counter variables. Use unsigned long primarily when interfacing directly with legacy Arduino core functions like millis() that explicitly return that type.
Rapid-Fire FAQ: Edge Cases and Troubleshooting
Can I print an unsigned long to the Serial Monitor?
Yes. Serial.print(myVar) handles 32-bit unsigned integers natively. However, if you are printing hex values for bitwise debugging, use Serial.print(myVar, HEX). Note that leading zeros are omitted by default; you will need a custom padding function if you require strict 8-character hex formatting.
What happens if I bitshift an unsigned long?
Bitshifting (<< or >>) works perfectly, provided you do not shift by more than 31 bits. Shifting a 1 into the 31st bit (the most significant bit) is safe with unsigned long. If you were using a signed long, setting the 31st bit would turn the number negative, which corrupts mathematical comparisons. Always use unsigned types for bitwise operations and register manipulation.
I need more than 49.7 days of uptime. What are my options?
If you are building a long-term environmental datalogger that requires continuous uptime tracking beyond 49.7 days without resetting, you must implement a software counter using a 64-bit integer (uint64_t or unsigned long long). You can achieve this by attaching an interrupt to the millis() overflow event or by checking for rollover in your main loop and incrementing a separate uint64_t 'days' counter. Note that 64-bit math on 8-bit AVR microcontrollers is computationally expensive and requires multiple clock cycles; avoid it inside high-frequency interrupt service routines (ISRs).
Why does my ESP32 code compile differently than my Uno code?
The ESP32 utilizes a 32-bit Xtensa or RISC-V architecture. On the ESP32, a standard int is 32 bits, meaning an int and a long share the exact same memory footprint and limits. However, relying on this is bad practice. Always write your code assuming the lowest common denominator (16-bit int) to ensure your sketch remains compatible if you ever migrate it back to an ATmega328P or ATtiny85 board.






