Postlad/Guides

Send ESP32 data to the cloud

The whole job is one HTTP request. Connect to WiFi, build a URL with your reading in the query string, call GET, done — no JSON, no SDK, no MQTT broker. Below is a complete sketch you can paste into a blank Arduino IDE window and run with no sensor attached, then a DHT22 version once you want real numbers.

Postlad is live. api.postlad.com answers today, so the sketch below sends to a real endpoint. What you need first is a write key: create a free account, click the link we email you, and it is waiting on your streams page. The sketch structure is the standard one and works against any GET-style endpoint; only the URL changes.


The core of it

Four lines do the actual work:

cpp
HTTPClient http;
http.begin(client, "http://api.postlad.com/u?k=YOUR_KEY&f1=23.5");
int status = http.GET();
http.end();

That is the entire integration. Everything else in the sketch below is WiFi setup and printing useful things to the serial monitor. If the http:// rather than https:// makes you twitch, that is covered below — it is deliberate, it is supported, and HTTPS works too.

Complete sketch — no sensor needed

Paste this into a new sketch, fill in the three constants at the top, and flash it. It sends a slowly wandering fake temperature so you can confirm the whole path works before you wire anything up.

Reviewed, not yet bench-tested on this exact board. If it misbehaves for you, tell us and we'll fix the page.

cpppostlad_fake.ino
#include <WiFi.h>
#include <HTTPClient.h>

// ---- fill these in -------------------------------------------------------
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* WRITE_KEY     = "YOUR_KEY";
// --------------------------------------------------------------------------

// Plain HTTP on purpose - see "About that http://" below. https:// also works.
const char* HOST_URL = "http://api.postlad.com/u";

// 30 s. The free tier's floor is one reading every 5 s, but its monthly quota
// is 100,000 points - which is one reading every 26 s, sustained, for a month.
const unsigned long SEND_INTERVAL_MS = 30000;

WiFiClient client;          // one plain TCP client, reused for every request
float fakeTemperature = 22.0;

void connectWiFi() {
  Serial.printf("Connecting to %s", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nConnected. IP address: %s\n", WiFi.localIP().toString().c_str());
}

void setup() {
  Serial.begin(115200);
  delay(200);
  connectWiFi();
}

void loop() {
  // A gentle random walk so the chart has something to draw.
  fakeTemperature += (random(-100, 101) / 100.0);
  if (fakeTemperature < 15.0) fakeTemperature = 15.0;
  if (fakeTemperature > 30.0) fakeTemperature = 30.0;

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  HTTPClient http;
  String url = String(HOST_URL) + "?k=" + WRITE_KEY + "&f1=" + String(fakeTemperature, 2);

  if (http.begin(client, url)) {
    int status = http.GET();
    if (status > 0) {
      Serial.printf("sent %.2f -> HTTP %d %s\n",
                    fakeTemperature, status, http.getString().c_str());
    } else {
      Serial.printf("request failed: %s\n", http.errorToString(status).c_str());
    }
    http.end();
  } else {
    Serial.println("http.begin() failed");
  }

  delay(SEND_INTERVAL_MS);
}

Open the serial monitor at 115200 baud. You should see a dotted connection line, then one line per reading with the HTTP status and the server's reply — which for a stored point is the plain text ok followed by the number of fields it took.

Board and library versions: written against the ESP32 Arduino core 3.x (WiFi.h and HTTPClient.h both ship with the core — nothing to install) in Arduino IDE 2.x, targeting a classic ESP32 WROOM-32 dev board. On core 2.x the same sketch compiles unchanged. Passing an explicit WiFiClient to http.begin() rather than calling http.begin(url) is what keeps it portable across both cores, and it is the same shape you need for the HTTPS variant.

Sending more than one value

The endpoint takes up to eight numeric fields, f1 through f8, in one request. Send them together rather than making eight requests — it is one round trip, one rate-limit slot, one point against your monthly quota, and the readings share a timestamp:

cpp
String url = String(HOST_URL) + "?k=" + WRITE_KEY
           + "&f1=" + String(temperature, 2)
           + "&f2=" + String(humidity, 1)
           + "&f3=" + String(pressure, 1);

The DHT22 version

Once the fake data shows up, swap in a real sensor. The DHT22 (AM2302) is the usual first choice: about $4, temperature and humidity on one pin.

Wiring — three connections, no breadboard gymnastics:

DHT22 pinESP32
VCC3V3
DATAGPIO 4
GNDGND

Most DHT22 breakout modules have the pull-up resistor fitted already. If you have a bare four-pin sensor, add a 10 kΩ resistor between DATA and 3V3.

Libraries: install DHT sensor library by Adafruit from the Library Manager, and accept the prompt to install Adafruit Unified Sensor alongside it.

Reviewed, not yet bench-tested on this exact board and sensor. If it misbehaves for you, tell us and we'll fix the page.

cpppostlad_dht22.ino
#include <WiFi.h>
#include <HTTPClient.h>
#include <DHT.h>

// ---- fill these in -------------------------------------------------------
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* WRITE_KEY     = "YOUR_KEY";
// --------------------------------------------------------------------------

const char* HOST_URL = "http://api.postlad.com/u";
const unsigned long SEND_INTERVAL_MS = 30000;

#define DHTPIN  4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

WiFiClient client;

void connectWiFi() {
  Serial.printf("Connecting to %s", WIFI_SSID);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nConnected. IP address: %s\n", WiFi.localIP().toString().c_str());
}

void setup() {
  Serial.begin(115200);
  delay(200);
  dht.begin();
  delay(2000);                   // the DHT22 needs a moment after power-up
  connectWiFi();
}

void loop() {
  float temperature = dht.readTemperature();   // °C; use readTemperature(true) for °F
  float humidity    = dht.readHumidity();

  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("DHT read failed - check wiring and the pull-up resistor");
    delay(2000);
    return;
  }

  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }

  HTTPClient http;
  String url = String(HOST_URL) + "?k=" + WRITE_KEY
             + "&f1=" + String(temperature, 2)
             + "&f2=" + String(humidity, 1);

  if (http.begin(client, url)) {
    int status = http.GET();
    if (status > 0) {
      Serial.printf("%.2f C  %.1f %%RH  -> HTTP %d %s\n",
                    temperature, humidity, status, http.getString().c_str());
    } else {
      Serial.printf("request failed: %s\n", http.errorToString(status).c_str());
    }
    http.end();
  }

  delay(SEND_INTERVAL_MS);
}

The DHT22 samples about once every two seconds at best, so there is no point calling it faster than that. At a 30-second send interval you have plenty of margin.

About that http://

The URL in both sketches is plain http://, not https://, and that is a deliberate choice rather than an oversight. api.postlad.com answers plain HTTP directly and never redirects you to HTTPS. That last part matters more than it sounds: an unexpected 301 to another scheme or host is the single most common way a microcontroller HTTP call fails silently, and it is exactly what makes the Google Apps Script route so frustrating on an ESP32.

What you get for it:

  • No TLS handshake, so no ~40 KB of TLS buffers, no certificate store, and no need for the device to know what time it is.
  • A request you can debug by pasting the URL into a browser, or by running curl against it, and see exactly what the board sees.
  • It works on boards whose TLS stack is marginal — an ESP8266 with a busy heap being the classic case.

And what you give up, stated plainly: anyone who can see traffic between your board and the internet can read both the reading and the write key. On your own home network, for a temperature, that is usually a fair trade. On a shared, campus or public network — or if the key is attached to something you would mind a stranger writing to — use HTTPS instead. The write key is a password; treat it like one, and do not paste a real one into a forum post or push it to a public repository.

The HTTPS variant

Same endpoint, same query string, one extra include and a different client. This is the honest version of what the sketch used to do:

cpp
#include <WiFiClientSecure.h>

const char* HOST_URL = "https://api.postlad.com/u";

WiFiClientSecure client;

void setup() {
  // ... WiFi first, as above ...

  // Option 1: encrypted, but NOT authenticated. The board does not check it is
  // talking to the server it thinks it is. Encryption without identity.
  client.setInsecure();

  // Option 2: encrypted AND authenticated - the right one if the data matters.
  // Set the clock first or validation fails on the certificate's start date,
  // then pin the issuer's root CA as a PEM string in the sketch:
  //   configTime(0, 0, "pool.ntp.org");
  //   client.setCACert(ROOT_CA_PEM);
}

Everything after that — http.begin(client, url), http.GET(), http.end() — is identical. If you are picking between them: plain HTTP on a home network, HTTPS with a pinned root CA on anything else, and setInsecure() only when you understand that it stops eavesdropping but not impersonation.

Troubleshooting

It connects to WiFi and then nothing happens. Almost always a 2.4 GHz issue: the ESP32 has no 5 GHz radio. If your router presents one SSID for both bands, some ESP32s cope and some do not. Try a 2.4 GHz-only SSID or a phone hotspot to isolate it.

http.GET() returns a negative number. Negative values are client-side errors, not HTTP status codes. -1 is a connection failure (DNS or the host being unreachable), -11 is a read timeout. http.errorToString() prints the readable version, which the sketch above already does.

HTTP 429. You are sending faster than the rate limit allows. On the Postlad free tier the floor is one reading every 5 seconds, with a burst of two forgiven, and the body of the response tells you how many seconds to wait. The sketch's 30-second interval leaves plenty of headroom. Increase SEND_INTERVAL_MS rather than retrying immediately — retrying into a rate limit just wastes battery. A 429 whose body says quota_exceeded is a different problem: that is the 100,000 points a month a free account gets, and it tells you when the counter resets.

HTTP 301 or 302 that never resolves. Classic when people point an ESP32 at a Google Apps Script endpoint: the script redirects to a different host and HTTPClient does not follow it across hosts by default. Call http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS) if you are stuck with a redirecting endpoint. It is one of the good reasons not to use a spreadsheet as a database, and it is why the endpoint in this page is documented as never redirecting.

The board reboots every few minutes. Watchdog reset, usually from a blocking WiFi reconnect loop. The while (WiFi.status() != WL_CONNECTED) loop above is fine at boot but will hang forever if the network is down — for an unattended device, add a counter and ESP.restart() after, say, 60 failed attempts.

Readings arrive but the timestamps drift. Don't fight it. delay() is not a clock and an ESP32 with no RTC has no idea what time it is. Let the server timestamp on arrival, which is what the endpoint above does. This is also why you do not need a DS3231 module for a logging project.

ESP8266 and other boards

The same sketch works on an ESP8266 (NodeMCU, Wemos D1 Mini) with two changes:

cpp
#include <ESP8266WiFi.h>          // instead of WiFi.h
#include <ESP8266HTTPClient.h>    // instead of HTTPClient.h

WiFiClient client;, http.begin(client, url) and http.GET() are identical. This is where plain HTTP earns its place: the ESP8266 has far less RAM than an ESP32, and TLS is the thing that tips a busy sketch into out-of-memory crashes. If you do want HTTPS on one, call client.setBufferSizes(1024, 1024) before connecting and keep the rest of your heap free.

For the Raspberry Pi and other Linux boards, the Python equivalent is five lines with requests. For the Raspberry Pi Pico W the MicroPython urequests module follows the same pattern.

Where the data goes

An HTTP GET is only half a data logger. The other half is whatever is on the receiving end, and that is where the choice actually matters — how long your history survives, whether you can show someone the chart without making them sign up, and whether the embed looks presentable in a write-up.

Postlad is the half we built: a stream gets a write key, the device sends the URL above, and the stream has a live page with charts and a share link that needs no account. The free tier keeps every reading at full detail for 30 days and then an hourly minimum, maximum and average for 12 months. Embeds and CSV export both work: the chart drops into a write-up as a script tag or an iframe, with a small Postlad badge on the free tier, and the export button is on the stream page.

FAQ

How do I send ESP32 data to the cloud without a library?

You still need the WiFi and HTTP client that ship with the ESP32 Arduino core, but you need nothing beyond them — no vendor SDK, no MQTT library, no JSON library, and with plain HTTP not even a TLS stack. The sketch above uses only WiFi.h and HTTPClient.h, both built in. That is the practical advantage of an endpoint that accepts a plain GET: the payload is the URL.

Should I use HTTP GET, POST or MQTT?

GET for simple periodic readings — it is the least code and the easiest to debug, because you can paste the URL into a browser and see what happens. POST when you are sending a batch of buffered readings or a payload too big for a URL. MQTT when you need the server to push things back to the device promptly, or you are sending many messages per second; it costs you a persistent connection and a broker, which is real complexity for a device that reports every 30 seconds.

How often can an ESP32 send data to the cloud?

The board can comfortably manage several requests per second on a decent WiFi link — the limit is almost always the service's rate limit, not the ESP32. ThingSpeak's free minimum interval is one reading every 15 seconds; ours is one every 5. For continuous logging on the free plan, start with one reading every 30 seconds for one stream, or every 60 seconds for each of two streams. Either schedule uses 89,280 readings in a 31-day month, within the 100,000 stored points shared by the account, provided there are no other stored readings that month. Faster intervals suit shorter experiments but use the allowance sooner. If you summarize faster samples on the device, remember that sending only an average loses the individual readings and can hide brief peaks.

Can I send ESP32 data to Google Sheets instead?

Yes, through an Apps Script web app with a doGet(e) handler, and plenty of tutorials cover it. It works until it doesn't: the redirect trap above bites most people on day one, Apps Script has daily quotas, and a spreadsheet with a hundred thousand rows of readings stops being pleasant to open. It is a good way to get data somewhere in an afternoon and a poor long-term logger.

Do I need an RTC module to timestamp my readings?

No. Let the server timestamp each reading as it arrives — that is what the endpoint above does, and it removes a module, a coin cell and a class of bugs. An RTC earns its place only when the device logs while offline and has to record when each buffered reading was taken. See the Raspberry Pi logger for how that buffering looks.

Create your free account

The endpoint this sketch points at is live: one-URL writes, a live chart, a share link that needs no account, and 30 days at full detail plus hourly summaries for 12 months on the free tier. What you need is a write key — enter an email, click the link we send back, and it is waiting on your streams page.

No password, no credit card, and we will tell you honestly if what we have built isn't what you wanted.

Create your free account →