433 MHz RF modules are some of the cheapest wireless components you can buy. A transmitter and receiver pair costs less than a dollar, works at ranges up to $100\,\text{m}$ in open air, and requires no network infrastructure -- no WiFi, no Bluetooth pairing, no broker. The trade-off is simplicity: these are one-way, low-bandwidth radio links with no built-in error checking, addressing, or acknowledgment. You transmit bits, and the receiver either catches them or does not.
These modules operate in the $433.92\,\text{MHz}$ ISM (Industrial, Scientific, Medical) band, which is license-free in most countries. They use ASK (Amplitude-Shift Keying) or OOK (On-Off Keying) modulation -- the transmitter is either transmitting a carrier signal (logic 1) or silent (logic 0). This is the same frequency and modulation used by many wireless doorbells, weather stations, car key fobs, and garage door openers.
🔗433 MHz vs Other Wireless Options
| Feature | 433 MHz RF | WiFi | BLE | ESP-NOW |
|---|---|---|---|---|
| Range | ~$100\,\text{m}$ (open air) | Depends on router (~$50\,\text{m}$) | ~$100\,\text{m}$ | ~$200\,\text{m}$ |
| Data rate | ~$2\,\text{kbps}$ (typical) | $11$--$150\,\text{Mbps}$ | $1$--$2\,\text{Mbps}$ | ~$1\,\text{Mbps}$ |
| Power consumption | Very low (TX: ~$20\,\text{mA}$, RX: ~$5\,\text{mA}$) | High (~$100$--$300\,\text{mA}$) | Low (~$10$--$50\,\text{mA}$) | Low |
| Direction | One-way per module | Bidirectional | Bidirectional | Bidirectional |
| Infrastructure | None | Router required | None | None |
| Addressing | None built-in | IP addresses | MAC + GATT | MAC addresses |
| Error checking | None built-in | TCP/IP handles it | BLE stack handles it | Built-in ACK |
| Cost per link | ~$1 (TX+RX pair) | Built into ESP32 | Built into ESP32 | Built into ESP32 |
| Frequency | $433\,\text{MHz}$ | $2.4\,\text{GHz}$ | $2.4\,\text{GHz}$ | $2.4\,\text{GHz}$ |
| Penetration through walls | Good (lower frequency) | Moderate | Moderate | Moderate |
Use 433 MHz when you need a cheap, long-range, one-way link and low bandwidth is acceptable -- remote sensors, wireless buttons, simple alerts. Use ESP-NOW, BLE, or WiFi when you need bidirectional communication, higher data rates, or more reliable delivery.
Note: The ESP32 does not have a built-in 433 MHz radio. You need separate transmitter and receiver modules. Each module is one-way: the transmitter only transmits, and the receiver only receives. For bidirectional communication, you need both modules on each ESP32.
🔗What You Need
| Component | Qty | Notes | Buy |
|---|---|---|---|
| ESP32 dev board | 2 | One for transmitter, one for receiver | AliExpress | Amazon.de .co.uk .com |
| 433 MHz transmitter module | 1 | ASK/OOK transmitter module (FS1000A or similar) | Amazon.de .co.uk .com |
| 433 MHz receiver module | 1 | ASK/OOK receiver module (XY-MK-5V or similar) | Amazon.de .co.uk .com |
| Jumper wires | ~6 | Male-to-male | AliExpress | Amazon.de .co.uk .com |
| Breadboard | 2 | One for each ESP32 | AliExpress | Amazon.de .co.uk .com |
Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.
🔗Wiring
🔗Transmitter (FS1000A or similar)
The transmitter module has three pins:
| Transmitter Pin | Connect to |
|---|---|
| VCC | ESP32 3.3V |
| GND | ESP32 GND |
| DATA (ATAD) | ESP32 GPIO 4 |
The transmitter works at 3.3V, though some modules accept up to 12V for increased range. At 3.3V the range is shorter but sufficient for testing and indoor use.
🔗Receiver (XY-MK-5V or similar)
The receiver module typically has four pins (two of which are DATA -- they are internally connected, so use either one):
| Receiver Pin | Connect to |
|---|---|
| VCC | ESP32 5V (Vin) |
| GND | ESP32 GND |
| DATA | ESP32 GPIO 2 |
Warning: The receiver module works best at $5\,\text{V}$. At $3.3\,\text{V}$ it will function but with significantly reduced sensitivity and range. However, ESP32 GPIO pins are not $5\,\text{V}$ tolerant -- the absolute maximum input voltage is $3.6\,\text{V}$. If your receiver is powered at $5\,\text{V}$, its DATA pin may output $5\,\text{V}$ logic. Use a voltage divider (two resistors, e.g., $10\,\text{k}\Omega$ and $20\,\text{k}\Omega$) or a logic level shifter on the DATA line to bring it down to $3.3\,\text{V}$. Alternatively, power the receiver at $3.3\,\text{V}$ for testing (safe but shorter range).
🔗Antenna
Both modules have a solder pad or through-hole labeled ANT. Soldering a straight wire antenna dramatically improves range. The ideal length for a quarter-wave antenna at $433\,\text{MHz}$ is:
$$\lambda / 4 = \frac{c}{4f} = \frac{3 \times 10^8}{4 \times 433.92 \times 10^6} \approx 17.3\,\text{cm}$$
Cut a $17.3\,\text{cm}$ piece of solid core wire and solder it to the ANT pad on both the transmitter and receiver. This single improvement can increase your range from a few meters to $50$--$100\,\text{m}$.
🔗Raw Transmission (Why It Fails)
Before using a proper encoding library, let's try the simplest possible approach: toggling a GPIO pin to send data. This intentionally fails to teach you why encoding matters.
The idea is straightforward -- set the transmitter's DATA pin HIGH or LOW to send ones and zeros, and read the receiver's DATA pin on the other side. Here is a minimal attempt:View raw transmission sketch (sender)
// RAW 433MHz Sender - intentionally unreliable!
// This demonstrates WHY you need an encoding library.
#define TX_PIN 4
void sendByte(uint8_t data) {
for (int i = 7; i >= 0; i--) {
if (data & (1 << i)) {
digitalWrite(TX_PIN, HIGH);
} else {
digitalWrite(TX_PIN, LOW);
}
delayMicroseconds(500); // ~2 kbps
}
}
void setup() {
Serial.begin(115200);
pinMode(TX_PIN, OUTPUT);
digitalWrite(TX_PIN, LOW);
Serial.println("Raw 433 MHz sender ready");
}
void loop() {
// Send a sync pattern so the receiver knows data is coming
for (int i = 0; i < 8; i++) {
digitalWrite(TX_PIN, HIGH);
delayMicroseconds(500);
digitalWrite(TX_PIN, LOW);
delayMicroseconds(500);
}
// Send the value 42 as a raw byte
sendByte(42);
// Pull low after transmission
digitalWrite(TX_PIN, LOW);
delay(2000);
Serial.println("Sent: 42");
}View raw transmission sketch (receiver)
// RAW 433MHz Receiver - intentionally unreliable!
// This demonstrates WHY you need an encoding library.
#define RX_PIN 2
uint8_t receiveByte() {
uint8_t data = 0;
for (int i = 7; i >= 0; i--) {
if (digitalRead(RX_PIN) == HIGH) {
data |= (1 << i);
}
delayMicroseconds(500);
}
return data;
}
bool waitForSync() {
// Look for alternating high/low pattern
int transitions = 0;
int lastState = digitalRead(RX_PIN);
for (int i = 0; i < 5000; i++) {
int state = digitalRead(RX_PIN);
if (state != lastState) {
transitions++;
lastState = state;
}
if (transitions >= 10) return true;
delayMicroseconds(100);
}
return false;
}
void setup() {
Serial.begin(115200);
pinMode(RX_PIN, INPUT);
Serial.println("Raw 433 MHz receiver ready");
}
void loop() {
if (waitForSync()) {
uint8_t value = receiveByte();
Serial.printf("Received: %d\n", value);
}
}
What happens when you try this: Sometimes it works, sometimes it prints garbage, and sometimes it receives phantom values when nobody is transmitting. The problems are fundamental:
- No clock recovery. The sender and receiver have no shared clock. Even a small timing drift causes bits to shift, corrupting every byte.
- No preamble or sync word. The receiver cannot tell where a message starts. It samples at arbitrary points and gets random data.
- No error detection. There is no checksum or CRC. If a bit is corrupted by noise, the receiver has no way to know.
- Ambient noise. The $433\,\text{MHz}$ band is full of signals from weather stations, car key fobs, doorbells, and other devices. The receiver's AGC (Automatic Gain Control) amplifies this noise when no valid signal is present, producing random HIGH/LOW transitions.
This is why every practical 433 MHz project uses an encoding library. The most popular one for Arduino is rc-switch.
🔗Discovering Interference
Before you start sending your own data, it helps to see how noisy the 433 MHz band actually is. This sketch counts how many transitions the receiver detects in each second, even with no transmitter nearby:View interference discovery sketch
// 433 MHz Interference Scanner
// Shows how much ambient noise exists on the 433 MHz band.
// Run this with NO transmitter powered on.
#define RX_PIN 2
volatile unsigned long pulseCount = 0;
void IRAM_ATTR onPulse() {
pulseCount++;
}
void setup() {
Serial.begin(115200);
pinMode(RX_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(RX_PIN), onPulse, CHANGE);
Serial.println("433 MHz Interference Scanner");
Serial.println("No transmitter should be powered on.");
Serial.println("Watching for ambient signals...");
Serial.println();
}
void loop() {
// Snapshot and reset the count every second
noInterrupts();
unsigned long count = pulseCount;
pulseCount = 0;
interrupts();
Serial.printf("Transitions in last second: %lu", count);
if (count < 100) {
Serial.println(" (quiet)");
} else if (count < 1000) {
Serial.println(" (some activity)");
} else {
Serial.println(" (NOISY - interference present)");
}
delay(1000);
}
You will likely see hundreds or thousands of transitions per second, even indoors with no transmitter active. This is the background noise that makes raw transmission unreliable. On a typical desk in a residential area, expect to see 200--2000 transitions per second from various sources:
| Source | Typical pattern |
|---|---|
| Weather stations | Short bursts every 30--60 seconds |
| Car key fobs | Brief spikes when neighbors lock/unlock cars |
| Wireless doorbells | Occasional bursts |
| Baby monitors | Periodic transmissions |
| General RF noise | Constant low-level activity |
This is exactly why encoding and protocol libraries exist -- they add structure that lets the receiver distinguish your signal from the noise.
🔗Reliable Communication with RC-Switch
The rc-switch library solves the problems we encountered with raw transmission. It encodes each value with a protocol that includes sync pulses, bit encoding with distinct HIGH/LOW timing, and repetition for reliability.
🔗How RC-Switch Works
- Sync pulse: A long LOW period that tells the receiver "a message is starting."
- Bit encoding: Each bit is encoded as a specific pattern of HIGH and LOW durations. A "1" might be a long HIGH followed by a short LOW, and a "0" is a short HIGH followed by a long LOW. This means timing drift within a bit does not corrupt it -- the receiver only needs to distinguish "long" from "short."
- Repetition: Each message is sent multiple times (default: 10). The receiver accepts the first valid decode and ignores duplicates.
- Pulse length: The base timing unit (default: ~$320\,\mu\text{s}$). All other timings are multiples of this.
🔗Installing the Library
In Arduino IDE: Sketch > Include Library > Manage Libraries, search for rc-switch by sui77, and install it. The library works with both the ESP32 and Arduino boards.
🔗Sender
View RC-Switch sender sketch
// 433 MHz Sender using RC-Switch
// Sends integer values that the receiver can reliably decode.
#include <RCSwitch.h>
#define TX_PIN 4
RCSwitch sender = RCSwitch();
uint32_t counter = 0;
void setup() {
Serial.begin(115200);
// Initialize the transmitter on the specified pin
sender.enableTransmit(TX_PIN);
// Optional: set protocol (1 is default, works with most receivers)
sender.setProtocol(1);
// Optional: set number of transmission repetitions (default is 10)
sender.setRepeatTransmit(10);
Serial.println("RC-Switch sender ready on GPIO 4");
}
void loop() {
// RC-Switch sends a 24-bit value (0 to 16,777,215)
// We'll send a counter value
uint32_t value = 1000 + counter;
sender.send(value, 24); // Send as 24-bit value
Serial.printf("Sent: %lu\n", value);
counter++;
if (counter > 9999) counter = 0;
delay(3000); // Wait 3 seconds between transmissions
}🔗Receiver
View RC-Switch receiver sketch
// 433 MHz Receiver using RC-Switch
// Receives and decodes values sent by the RC-Switch sender.
#include <RCSwitch.h>
#define RX_PIN 2
RCSwitch receiver = RCSwitch();
void setup() {
Serial.begin(115200);
// The receiver uses interrupts. On ESP32, specify the GPIO pin directly.
receiver.enableReceive(digitalPinToInterrupt(RX_PIN));
Serial.println("RC-Switch receiver ready on GPIO 2");
Serial.println("Waiting for signals...");
}
void loop() {
if (receiver.available()) {
uint32_t value = receiver.getReceivedValue();
if (value == 0) {
Serial.println("Received unknown encoding");
} else {
Serial.printf("Received: %lu (protocol: %d, bits: %d)\n",
value,
receiver.getReceivedProtocol(),
receiver.getReceivedBitlength());
}
receiver.resetAvailable(); // Ready for next message
}
}Upload the receiver sketch to one ESP32 and the sender sketch to the other. Open the receiver's Serial Monitor. You should see the counter values arriving reliably every 3 seconds.
Tip: RC-Switch can also receive signals from commercial 433 MHz devices like wireless doorbells and remote-controlled power outlets. Run the receiver sketch near these devices and press their buttons to capture the codes. You can then retransmit those codes to control the devices from your ESP32.
🔗RC-Switch Limitations
| Limitation | Detail |
|---|---|
| Data size | 24-bit value per transmission ($0$ to $16{,}777{,}215$) |
| Direction | One-way (no acknowledgment) |
| Throughput | ~1 message per second is practical (with 10 repetitions) |
| Addressing | No built-in addressing -- all receivers hear all transmissions |
| Encryption | None |
🔗Sending Sensor Data
RC-Switch sends a single integer (up to 24 bits). To send sensor data, you need to map your readings into that integer space. A common approach is to encode multiple values into a single number:
// Encoding: pack a device ID and temperature into 24 bits
// Bits 23-20: device ID (0-15, supports 16 devices)
// Bits 19-0: temperature * 100 (0-1048575, covers -100.00 to +10385.75)
uint8_t deviceID = 1;
float temperature = 23.45;
int32_t tempEncoded = (int32_t)(temperature * 100); // 2345
uint32_t payload = ((uint32_t)deviceID << 20) | (tempEncoded & 0xFFFFF);
sender.send(payload, 24);On the receiver side, decode the value:
uint32_t received = receiver.getReceivedValue();
uint8_t deviceID = (received >> 20) & 0x0F;
int32_t tempRaw = received & 0xFFFFF;
// Sign-extend if negative (bit 19 set)
if (tempRaw & 0x80000) {
tempRaw |= 0xFFF00000;
}
float temperature = tempRaw / 100.0;
Serial.printf("Device %d: %.2f C\n", deviceID, temperature);Warning: Do not transmit continuously. The $433\,\text{MHz}$ ISM band has duty cycle regulations in many countries (typically 1% in Europe, meaning you can transmit for at most $36\,\text{s}$ per hour). Even without regulations, continuous transmission drowns out other devices on the same frequency and drains batteries quickly. Send readings at intervals of 10 seconds or more.
🔗Range and Reliability Tips
| Factor | Effect on Range | Recommendation |
|---|---|---|
| Antenna | No antenna: $1$--$3\,\text{m}$. With $17.3\,\text{cm}$ wire: $30$--$100\,\text{m}$ | Always solder an antenna on both TX and RX |
| Supply voltage (TX) | 3.3V: reduced power. 5V: normal. 12V: maximum range | Use 5V or higher for the transmitter if range matters (ESP32 GPIO is still 3.3V for DATA) |
| Supply voltage (RX) | 3.3V: reduced sensitivity. 5V: optimal | Always power receiver at 5V |
| Obstacles | Walls reduce range by $30$--$70%$ per wall | $433\,\text{MHz}$ penetrates walls better than $2.4\,\text{GHz}$ but is still affected |
| Orientation | Antenna orientation affects radiation pattern | Keep antennas vertical and parallel to each other |
| Repetitions | More repetitions = higher chance of at least one getting through | RC-Switch default of 10 is good; increase to 15--20 for long range |
| Interference | Other 433 MHz devices, electrical noise, metal objects | Move away from computers, motors, LED drivers |
| Baud rate | Lower baud rate = longer pulses = more noise immunity | RC-Switch's default protocol timing is already conservative |
🔗Expected Range by Scenario
| Scenario | Typical Range |
|---|---|
| No antenna, 3.3V, indoors | $1$--$3\,\text{m}$ |
| Wire antenna, 3.3V, indoors | $10$--$20\,\text{m}$ |
| Wire antenna, 5V TX, indoors (1--2 walls) | $15$--$30\,\text{m}$ |
| Wire antenna, 5V TX, open air line of sight | $50$--$100\,\text{m}$ |
| Wire antenna, 12V TX, open air line of sight | $100$--$200\,\text{m}$ |
🔗Common Issues
| Problem | Cause | Fix |
|---|---|---|
| No signal received at all | Wiring error, wrong pin in code, no antenna | Double-check wiring; verify GPIO pin numbers; solder antennas |
| Receiver prints random values with no transmitter | Normal -- AGC amplifies ambient noise | This is expected. Use RC-Switch; it only reports properly encoded messages |
| Very short range ($< 3\,\text{m}$) | No antenna, or receiver powered at 3.3V | Add $17.3\,\text{cm}$ wire antenna; power receiver from 5V |
| Intermittent reception | Interference from other 433 MHz sources, or edge of range | Increase setRepeatTransmit(); add antenna; reduce distance |
receiver.available() never true | Interrupt not attached, or wrong pin | Ensure you use digitalPinToInterrupt(RX_PIN) and not just the raw pin number |
| Values corrupted / wrong number | Bit length mismatch between sender and receiver | Use the same bit length (e.g., 24) on both sides |
| RC-Switch receives codes from unknown sources | Other 433 MHz devices in the area | Filter by expected protocol or value range in your code |
| Transmitter gets hot | Supply voltage too high or continuous transmission | Keep TX voltage within module specs; add delays between sends |
🔗What's Next
Once you outgrow 433 MHz or need bidirectional communication, the ESP32's built-in radios offer more capable alternatives:
- ESP-NOW Protocol -- direct ESP32-to-ESP32 communication, bidirectional, up to $250\,\text{bytes}$ per message, no infrastructure needed
- MQTT Protocol -- publish/subscribe messaging over WiFi, ideal for dashboards, logging, and home automation
- BLE Protocol -- Bluetooth Low Energy for phone-to-ESP32 communication, sensor beacons, and low-power wireless