Adafruit IO is a free cloud service that makes it easy to get your sensor data onto a web dashboard — no backend coding, no server management. In this tutorial, you will create feeds for temperature, humidity, and pressure, publish data from a BME280 sensor via HTTP, and build a live dashboard with charts and gauges. Everything here works on the free tier.
🔗What is Adafruit IO?
Adafruit IO is a cloud platform built by Adafruit for IoT projects. It accepts data via MQTT or a simple HTTP REST API, stores it, and lets you visualize it with built-in dashboard widgets.
Key features of the free tier:
| Feature | Free Tier Limit |
|---|---|
| Data rate | 30 data points per minute (across all feeds) |
| Data retention | 30 days |
| Dashboards | 5 |
| Feeds | 10 |
| Triggers (alerts) | 5 |
The free tier is generous enough for most hobby projects. A single ESP32 sending temperature, humidity, and pressure every 30 seconds uses only 6 data points per minute — well within the limit.
🔗What You'll Need
| Component | Qty | Notes | Buy |
|---|---|---|---|
| ESP32 dev board | 1 | AliExpress | Amazon.de .co.uk .com | |
| BME280 sensor module | 1 | AliExpress | Amazon.de .co.uk .com | |
| Breadboard | 1 | AliExpress | Amazon.de .co.uk .com | |
| Jumper wires | ~4 | Male-to-male | 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.
You will also need:
- A free Adafruit IO account — sign up at io.adafruit.com
- The Arduino IDE with ESP32 board support installed
🔗Required Libraries
Install the following through the Arduino Library Manager (Sketch > Include Library > Manage Libraries...):
- Adafruit BME280 Library by Adafruit
- Adafruit Unified Sensor by Adafruit (installed as a dependency)
- ArduinoJson by Benoit Blanchon (for the multi-feed example)
The HTTP requests use the built-in WiFi.h and HTTPClient.h libraries — no extra installation needed for those.
🔗Account Setup
- Go to io.adafruit.com and create a free account.
- Once logged in, click the key icon (or go to Settings > My Key) to find your AIO Key. You will need two values:
- Username — your Adafruit IO username
- Active Key — a long string like
aio_AbCd1234...
- Create a new Feed called
temperature(go to Feeds > New Feed).
Warning: Your AIO Key grants full access to your account. Treat it like a password — do not commit it to a public repository or share it in screenshots.
🔗Wiring
Connect the BME280 to your ESP32 using I2C. This is the same wiring used in the BME280 guide.
| BME280 Pin | ESP32 Pin | Notes |
|---|---|---|
| VIN / VCC | 3.3V | Use 3.3V (most modules have an onboard regulator) |
| GND | GND | |
| SDA | GPIO 21 | Default I2C data line on ESP32 |
| SCL | GPIO 22 | Default I2C clock line on ESP32 |
Pin labels and GPIO numbers vary between ESP32 boards. GPIOs 21 (SDA) and 22 (SCL) are the defaults on most ESP32-WROOM-32 DevKit boards. Check your board's pinout diagram if readings fail.
🔗Code Example: HTTP POST to Adafruit IO
This sketch connects to WiFi, reads the BME280 sensor, and sends the temperature to your Adafruit IO feed every 30 seconds using an HTTP POST request.View complete sketch
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// ----- WiFi credentials -----
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// ----- Adafruit IO credentials -----
const char* aioUsername = "YOUR_AIO_USERNAME";
const char* aioKey = "YOUR_AIO_KEY";
const char* feedName = "temperature";
// ----- Sensor -----
#define BME280_ADDRESS 0x76 // Change to 0x77 if needed
Adafruit_BME280 bme;
// ----- Timing -----
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 30000; // 30 seconds
void connectToWiFi() {
Serial.printf("Connecting to %s", ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nConnected! IP: %s\n",
WiFi.localIP().toString().c_str());
} else {
Serial.println("\nFailed to connect to WiFi.");
}
}
void sendToAdafruitIO(const char* feed, float value) {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi not connected. Skipping send.");
return;
}
HTTPClient http;
// Build the URL: https://io.adafruit.com/api/v2/{username}/feeds/{feed}/data
String url = "https://io.adafruit.com/api/v2/";
url += aioUsername;
url += "/feeds/";
url += feed;
url += "/data";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-AIO-Key", aioKey);
// Send the value as JSON
String payload = "{\"value\":" + String(value, 1) + "}";
int httpCode = http.POST(payload);
if (httpCode == 200) {
Serial.printf("Sent to '%s': %.1f\n", feed, value);
} else {
Serial.printf("Error sending to '%s': HTTP %d\n", feed, httpCode);
Serial.println(http.getString());
}
http.end();
}
void setup() {
Serial.begin(115200);
delay(1000);
connectToWiFi();
if (!bme.begin(BME280_ADDRESS)) {
Serial.println("Error: Could not find BME280 sensor.");
Serial.println("Check wiring or try address 0x77.");
while (1) delay(10);
}
Serial.println("BME280 sensor found. Starting data upload.");
}
void loop() {
// Reconnect WiFi if needed
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi lost. Reconnecting...");
connectToWiFi();
return;
}
unsigned long now = millis();
if (now - lastSendTime >= sendInterval) {
lastSendTime = now;
float temperature = bme.readTemperature();
Serial.printf("Temperature: %.1f °C\n", temperature);
sendToAdafruitIO(feedName, temperature);
}
}
🔗How the Code Works
WiFi connection — the
connectToWiFi()function connects to your network with a 10-second timeout, following the same pattern from the WiFi basics guide.BME280 initialization — the sensor is set up in
setup()using I2C. If it is not found, the sketch halts with an error message.HTTP POST — the
sendToAdafruitIO()function builds the Adafruit IO REST API URL, adds theX-AIO-Keyheader for authentication, and sends the sensor value as a JSON payload.Timing — data is sent every 30 seconds using
millis(), which avoids blocking the loop. This is well within the free tier's 30 data points per minute limit.
Tip: The free tier allows 30 data points per minute across all feeds combined. If you are sending temperature, humidity, and pressure every 30 seconds, that is 6 points per minute — plenty of headroom. But if you set the interval to 5 seconds across three feeds, you will hit the limit (36 points/minute) and start getting 429 errors.
🔗Building a Dashboard
Once data is flowing into your feed, you can visualize it:
- In Adafruit IO, go to Dashboards > New Dashboard. Give it a name (e.g., "ESP32 Environment").
- Open the dashboard and click the gear icon, then Create New Block.
- Choose Line Chart and select your
temperaturefeed. This gives you a time-series graph of temperature over time. - Click Create New Block again and choose Gauge. Select the
temperaturefeed, set the min/max range (e.g., $-10$ to $50$), and choose a color scheme. - Arrange the blocks by dragging them around. Click the gear icon and choose Edit Layout to resize them.
The dashboard updates in real time — as your ESP32 sends new data points, the chart and gauge update within seconds. You can share the dashboard link with others, and it works on mobile browsers too.
🔗Sending Multiple Feeds
Most sensor projects need more than one data stream. The BME280 gives us temperature, humidity, and pressure — let's send all three to separate feeds.
First, create three feeds in Adafruit IO:
temperaturehumiditypressure
Then use this modified sketch:View complete sketch — multiple feeds
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
// ----- WiFi credentials -----
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
// ----- Adafruit IO credentials -----
const char* aioUsername = "YOUR_AIO_USERNAME";
const char* aioKey = "YOUR_AIO_KEY";
// ----- Feed names -----
const char* feedTemperature = "temperature";
const char* feedHumidity = "humidity";
const char* feedPressure = "pressure";
// ----- Sensor -----
#define BME280_ADDRESS 0x76
Adafruit_BME280 bme;
// ----- Timing -----
unsigned long lastSendTime = 0;
const unsigned long sendInterval = 30000; // 30 seconds
void connectToWiFi() {
Serial.printf("Connecting to %s", ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nConnected! IP: %s\n",
WiFi.localIP().toString().c_str());
} else {
Serial.println("\nFailed to connect to WiFi.");
}
}
bool sendToAdafruitIO(const char* feed, float value) {
if (WiFi.status() != WL_CONNECTED) return false;
HTTPClient http;
String url = "https://io.adafruit.com/api/v2/";
url += aioUsername;
url += "/feeds/";
url += feed;
url += "/data";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-AIO-Key", aioKey);
String payload = "{\"value\":" + String(value, 1) + "}";
int httpCode = http.POST(payload);
bool success = (httpCode == 200);
if (!success) {
Serial.printf("Error sending to '%s': HTTP %d\n", feed, httpCode);
}
http.end();
return success;
}
void setup() {
Serial.begin(115200);
delay(1000);
connectToWiFi();
if (!bme.begin(BME280_ADDRESS)) {
Serial.println("Error: Could not find BME280 sensor.");
Serial.println("Check wiring or try address 0x77.");
while (1) delay(10);
}
Serial.println("BME280 found. Sending to 3 Adafruit IO feeds.");
}
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi lost. Reconnecting...");
connectToWiFi();
return;
}
unsigned long now = millis();
if (now - lastSendTime >= sendInterval) {
lastSendTime = now;
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F; // Pa to hPa
Serial.printf("Temp: %.1f °C | Humidity: %.1f %% | Pressure: %.1f hPa\n",
temperature, humidity, pressure);
sendToAdafruitIO(feedTemperature, temperature);
sendToAdafruitIO(feedHumidity, humidity);
sendToAdafruitIO(feedPressure, pressure);
}
}
This sketch calls sendToAdafruitIO() three times per cycle — once for each feed. At a 30-second interval, that is 6 data points per minute, leaving plenty of room within the free tier's limit.
🔗Group Feed Alternative
If you want to send all values in a single HTTP request (using only 1 data point instead of 3), Adafruit IO supports group feeds. Create a group, add your feeds to it, then POST JSON with all values at once:
void sendGroupData(float temp, float hum, float pres) {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient http;
String url = "https://io.adafruit.com/api/v2/";
url += aioUsername;
url += "/groups/environment/data";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.addHeader("X-AIO-Key", aioKey);
// Send all feeds in one request
String payload = "{\"feeds\":[";
payload += "{\"key\":\"temperature\",\"value\":\"" + String(temp, 1) + "\"},";
payload += "{\"key\":\"humidity\",\"value\":\"" + String(hum, 1) + "\"},";
payload += "{\"key\":\"pressure\",\"value\":\"" + String(pres, 1) + "\"}";
payload += "]}";
int httpCode = http.POST(payload);
Serial.printf("Group send: HTTP %d\n", httpCode);
http.end();
}This approach is more efficient with your rate limit — one request sends all three values. Create the group in the Adafruit IO web interface under Feeds > New Group, then add your existing feeds to it.
🔗Receiving Commands (Optional)
Adafruit IO feeds are bidirectional — you can also read values back. This lets you control your ESP32 from the dashboard. For example, you could create a Toggle feed and add a toggle button to your dashboard, then poll the feed from your ESP32 to turn an LED on or off.
Here is the basic pattern for polling a feed via HTTP GET:
String getFromAdafruitIO(const char* feed) {
if (WiFi.status() != WL_CONNECTED) return "";
HTTPClient http;
String url = "https://io.adafruit.com/api/v2/";
url += aioUsername;
url += "/feeds/";
url += feed;
url += "/data/last";
http.begin(url);
http.addHeader("X-AIO-Key", aioKey);
int httpCode = http.GET();
String result = "";
if (httpCode == 200) {
// The response is JSON — extract the "value" field
String response = http.getString();
int valueStart = response.indexOf("\"value\":\"") + 9;
int valueEnd = response.indexOf("\"", valueStart);
result = response.substring(valueStart, valueEnd);
}
http.end();
return result;
}You could call this in your loop() every few seconds and check whether the value is "1" (on) or "0" (off). For more responsive control, consider using MQTT instead of HTTP polling — MQTT delivers messages instantly when a feed value changes.
🔗Troubleshooting
| Problem | Possible Cause | Solution |
|---|---|---|
| HTTP 401 (Unauthorized) | Wrong AIO Key | Double-check the key in Settings > My Key — make sure you copied the full string |
| HTTP 403 (Forbidden) | Wrong username or feed name | Verify the username and feed key match exactly (feed keys are lowercase, use hyphens not spaces) |
| HTTP 429 (Too Many Requests) | Exceeded free tier rate limit | Increase the send interval — 30 data points/min across all feeds |
| Data not appearing on dashboard | Feed name mismatch | The feed key (shown in URL) may differ from the display name — use the key |
| WiFi connection fails | Wrong credentials or weak signal | Check SSID/password; try moving closer to the router |
| BME280 not found | Wrong I2C address or wiring | Try address 0x77; verify SDA/SCL connections |
| Values appear but chart is flat | All values are the same | Breathe on the sensor to change temperature/humidity and confirm updates |
🔗What's Next?
- ntfy.sh Notifications — add instant push alerts to your phone when a sensor value crosses a threshold
- MQTT Protocol Reference — use MQTT instead of HTTP for lower overhead and real-time bidirectional communication
- Home Assistant Setup — integrate your ESP32 sensors into a full home automation system