Build a Long-Range ESP32 LoRa Sensor Node for Off-WiFi-Grid Monitoring
WiFi has real range limits — a sensor at the far end of a property, in a detached shed, or anywhere beyond your router's reach simply won't connect reliably. LoRa (Long Range radio) solves this differently: low bandwidth, but genuine multi-kilometer range on tiny power draw, making it the right tool for scattered sensor nodes that just need to report small amounts of data (temperature, a door state, a battery level) back to a central point.
LoRa vs WiFi: When to Use Which
WiFiLoRa RangeTens of meters typically, less through wallsKilometers in open terrain, hundreds of meters through typical obstruction BandwidthHigh — video, large payloads, web requestsVery low — small packets only, not for streaming or large data Power drawRelatively high, especially for connection maintenanceVery low — genuinely enables multi-month battery life on small nodes Good forAnything needing real bandwidth or already near your networkRemote sensors, anything battery-powered and infrequent-reportingThe rule of thumb: if a sensor is out of practical WiFi range, or needs to run on battery for months instead of days, LoRa is very likely the better fit than trying to extend WiFi coverage with repeaters.
Hardware
ComponentNotes ESP32 LoRa dev boardBoards combining an ESP32 and a LoRa radio (commonly SX1276/SX1278-based) on one PCB simplify wiring significantly versus separate modules LoRa antennaMatched to your radio's frequency band — don't run a LoRa radio without an antenna attached, it can damage the radio module Sensors (per node)Whatever you're actually monitoring — temperature/humidity, door reed switch, soil moisture, etc. Battery + solar (optional)LoRa's low power draw makes solar-trickle-charged battery nodes genuinely practical for permanent outdoor placement Gateway nodeA second LoRa-equipped device (another ESP32, or a Raspberry Pi with a LoRa HAT) that receives from all your sensor nodes and forwards data onward, e.g. to your home network or Home AssistantImportant: Check Your Local Frequency Regulations
LoRa operates in different unlicensed ISM bands depending on region — commonly 915MHz in North America, 868MHz in Europe, and others elsewhere. Using hardware and firmware configured for the wrong region's frequency isn't just non-functional, it can be a real regulatory violation. Confirm your module and any firmware/library frequency configuration matches your actual region before transmitting.
Sensor Node Firmware
#include <SPI.h> #include <LoRa.h> #define LORA_FREQ 915E6 // match to YOUR region void setup() { Serial.begin(115200); if (!LoRa.begin(LORA_FREQ)) { Serial.println("LoRa init failed"); while (1); } LoRa.setSpreadingFactor(9); // range/reliability vs speed tradeoff, higher = longer range, slower LoRa.setSignalBandwidth(125E3); } void sendReading(float temperature, int battery) { LoRa.beginPacket(); LoRa.print("T:" + String(temperature) + ",B:" + String(battery)); LoRa.endPacket(); } void loop() { float temp = readTemperature(); // your sensor's actual read function int battery = readBatteryPercent(); sendReading(temp, battery); // Deep sleep between readings is where the real battery-life gains come from esp_sleep_enable_timer_wakeup(600 * 1000000ULL); // 10 minutes, in microseconds esp_deep_sleep_start(); }Deep sleep between transmissions is the single biggest factor in node battery life — an ESP32 actively running draws vastly more current than one in deep sleep, so a node that wakes, reads a sensor, transmits, and immediately sleeps again can run for months on a modest battery, while one left continuously awake would last days at best.
Gateway Firmware (Receiving Node)
#include <SPI.h> #include <LoRa.h> #include <WiFi.h> #include <HTTPClient.h> void setup() { Serial.begin(115200); LoRa.begin(915E6); // match sensor node frequency WiFi.begin("your-ssid", "your-password"); while (WiFi.status() != WL_CONNECTED) delay(500); } void loop() { int packetSize = LoRa.parsePacket(); if (packetSize) { String received = ""; while (LoRa.available()) { received += (char)LoRa.read(); } forwardToServer(received); } } void forwardToServer(String data) { HTTPClient http; http.begin("http://your-home-server/api/sensor-data"); http.addHeader("Content-Type", "text/plain"); http.POST(data); http.end(); }The gateway bridges the two worlds: it listens on LoRa for incoming sensor packets, then forwards them over WiFi to wherever you're actually storing/displaying the data (Home Assistant, a database, a simple logging script).
Tuning Range vs Battery Life
LoRa's spreading factor (SF) setting is the key tradeoff knob:
Spreading FactorRangeTransmission Time / Power Cost SF7 (lower)Shorter rangeFaster transmission, less airtime, less power per send SF12 (higher)Longer rangeSlower transmission, more airtime, more power per sendStart with a middle value (SF9 or SF10) and adjust based on your actual measured range needs — there's no reason to run maximum spreading factor (and its associated power cost) if your nodes are well within range at a lower setting.
Multiple Nodes: Addressing
With more than one sensor node reporting to the same gateway, include a node identifier in each transmission so the gateway (and whatever's storing the data) can tell nodes apart:
LoRa.print("NODE:shed_door,T:" + String(temperature) + ",B:" + String(battery));Parse this identifier on the gateway side to route data to the correct sensor entity in whatever system you're feeding it into.
Common Issues
SymptomLikely Cause No packets received at gatewayFrequency mismatch between nodes and gateway, or antenna not properly connected Range much shorter than expectedSpreading factor too low for the actual distance, antenna orientation/placement, or physical obstructions (LoRa still needs reasonable line-of-sight for best range, especially at higher frequencies) Battery draining faster than expectedDeep sleep not actually engaging correctly — verify with a multimeter that current draw drops significantly during the sleep period, not just that the code calls the sleep function Occasional dropped packetsNormal for LoRa at longer range — design your application to tolerate occasional missed readings rather than treating every packet as guaranteed deliveryOnce the node/gateway pattern is working for one sensor, adding more is just repeating the node firmware with a different identifier — this scales naturally into a genuinely useful property-wide sensor network without needing WiFi coverage everywhere you want to monitor.
Related Guides
- ESP32 Deep Sleep & Battery Optimization for Solar/Battery Projects
- Buck, Boost, and Buck-Boost Converters Explained: Choosing and Wiring a Switching Regulator
- nRF24L01 Wireless Modules: Cheap 2.4GHz Point-to-Point Control for Arduino and ESP32
- ESP32 LoRaWAN and The Things Network: OTAA Join, Payload Decoding, and Downlinks
- Battery Fuel Gauge ICs for Lithium Projects: MAX17048, BQ27441, and Coulomb Counting Explained
- Driving E-Paper Displays with ESP32 and Arduino: SPI Wiring, GxEPD2, and Partial Refresh
- Scaling Up: A Multi-Node LoRa Sensor Network with a Raspberry Pi Gateway
- Build a Battery-Powered ESP32 Smart Mailbox Notifier