Connecting your ESP32 to AWS IoT Core over a secure MQTT channel is one of the most practical skills in production IoT development. This guide walks you through every step — from creating your AWS Thing to publishing sensor data and reading Device Shadow state — with complete, working Arduino code.
Prerequisites
Before starting, make sure you have the following ready:
- ESP32 development board (any variant: DevKit, WROOM, WROVER)
- Active AWS account (free tier is sufficient)
- Arduino IDE 2.x or PlatformIO with ESP32 board support installed
- Libraries: PubSubClient by Nick O'Leary, ArduinoJson by Benoît Blanchon
- WiFi network credentials
| Library | Version | Install via |
|---|---|---|
| PubSubClient | 2.8+ | Arduino Library Manager |
| ArduinoJson | 6.x | Arduino Library Manager |
| WiFiClientSecure | built-in | ESP32 Arduino core |
Step 1 — Create an AWS IoT Thing
Log into the AWS Console and navigate to IoT Core → Manage → Things. Click Create things → Create a single thing.
- Enter a Thing name — e.g.
ESP32_Device_01 - Leave Thing type and groups as default for now
- Click Next to proceed to certificate creation
Step 2 — Generate Certificates
On the certificate page select "Auto-generate a new certificate". AWS will generate three files — download all of them before clicking Done, as you cannot download them again.
xxxx-certificate.pem.crt— your device certificatexxxx-private.pem.key— your private keyAmazonRootCA1.pem— the AWS root CA
Step 3 — Attach a Policy
Before activating the certificate you need to attach an IoT Policy. Create a new policy under Security → Policies with the following JSON. This grants your device permission to connect, publish, subscribe, and receive on any topic under its Thing name.
// AWS IoT Policy (JSON){
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iot:Connect",
"iot:Publish",
"iot:Subscribe",
"iot:Receive"
],
"Resource": "arn:aws:iot:YOUR_REGION:YOUR_ACCOUNT_ID:*"
}
]
}
Step 4 — Find Your Endpoint
In AWS IoT Core, go to Settings. Copy your Device data endpoint. It looks like:
xxxxxxxxxxxxxxx-ats.iot.us-east-1.amazonaws.com
This is your MQTT broker address. Save it — you'll need it in the sketch.
Step 5 — Prepare the Certificates for Arduino
You need to embed the three certificate files as C string literals in your sketch. Open each .pem file in a text editor. The format for the certificate in your Arduino .h file is:
// secrets.h
// NEVER commit this file to a public repository!
#define AWS_IOT_ENDPOINT "xxxxxxxxxxxxxxx-ats.iot.us-east-1.amazonaws.com"
#define AWS_IOT_PORT 8883
#define WIFI_SSID "YourWiFiSSID"
#define WIFI_PASSWORD "YourWiFiPassword"
#define THING_NAME "ESP32_Device_01"
// Amazon Root CA 1
static const char AWS_CERT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
... (paste your AmazonRootCA1.pem content here)
-----END CERTIFICATE-----
)EOF";
// Device Certificate
static const char AWS_CERT_CRT[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
... (paste your xxxx-certificate.pem.crt content here)
-----END CERTIFICATE-----
)EOF";
// Device Private Key
static const char AWS_CERT_PRIVATE[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
... (paste your xxxx-private.pem.key content here)
-----END RSA PRIVATE KEY-----
)EOF";
Step 6 — Full ESP32 Sketch
Create a new Arduino sketch and add the following code. This connects to WiFi, establishes a TLS connection to AWS IoT Core, publishes sensor data every 10 seconds, and subscribes to Device Shadow updates.
// main.ino — Full ESP32 AWS IoT Sketch#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include "secrets.h"
// MQTT Topics
#define PUBLISH_TOPIC "$aws/things/" THING_NAME "/shadow/update"
#define SUBSCRIBE_TOPIC "$aws/things/" THING_NAME "/shadow/update/delta"
WiFiClientSecure net;
PubSubClient client(net);
unsigned long lastPublish = 0;
const long PUBLISH_INTERVAL = 10000; // 10 seconds
// ----------------------------------------------------------
// Called whenever a message arrives on a subscribed topic
// ----------------------------------------------------------
void messageHandler(char* topic, byte* payload, unsigned int length) {
Serial.print("[MQTT] Message on topic: ");
Serial.println(topic);
StaticJsonDocument<512> doc;
deserializeJson(doc, payload, length);
// Example: respond to desired LED state from Device Shadow
if (doc["state"]["led"].is<bool>()) {
bool ledState = doc["state"]["led"];
digitalWrite(LED_BUILTIN, ledState ? HIGH : LOW);
Serial.printf("[Shadow] LED set to: %s\n", ledState ? "ON" : "OFF");
}
}
// ----------------------------------------------------------
// Connect to WiFi
// ----------------------------------------------------------
void connectWifi() {
Serial.printf("[WiFi] Connecting to %s", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.printf("\n[WiFi] Connected. IP: %s\n", WiFi.localIP().toString().c_str());
}
// ----------------------------------------------------------
// Connect to AWS IoT Core
// ----------------------------------------------------------
void connectAWS() {
// Load certificates from PROGMEM
net.setCACert(AWS_CERT_CA);
net.setCertificate(AWS_CERT_CRT);
net.setPrivateKey(AWS_CERT_PRIVATE);
client.setServer(AWS_IOT_ENDPOINT, AWS_IOT_PORT);
client.setCallback(messageHandler);
client.setBufferSize(1024);
Serial.print("[AWS] Connecting to IoT Core");
while (!client.connected()) {
if (client.connect(THING_NAME)) {
Serial.println("\n[AWS] Connected!");
client.subscribe(SUBSCRIBE_TOPIC);
Serial.printf("[AWS] Subscribed to: %s\n", SUBSCRIBE_TOPIC);
} else {
Serial.printf(".\n[AWS] Failed, rc=%d. Retrying in 5s...\n", client.state());
delay(5000);
}
}
}
// ----------------------------------------------------------
// Publish sensor data to Device Shadow
// ----------------------------------------------------------
void publishSensorData() {
float temperature = 22.5 + random(-20, 20) / 10.0; // Simulated sensor
float humidity = 60.0 + random(-50, 50) / 10.0;
StaticJsonDocument<256> doc;
JsonObject state = doc.createNestedObject("state");
JsonObject reported = state.createNestedObject("reported");
reported["temperature"] = serialized(String(temperature, 1));
reported["humidity"] = serialized(String(humidity, 1));
reported["wifi_rssi"] = WiFi.RSSI();
reported["uptime_s"] = millis() / 1000;
char jsonBuffer[256];
serializeJson(doc, jsonBuffer);
if (client.publish(PUBLISH_TOPIC, jsonBuffer)) {
Serial.printf("[MQTT] Published: %s\n", jsonBuffer);
} else {
Serial.println("[MQTT] Publish failed");
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_BUILTIN, OUTPUT);
connectWifi();
connectAWS();
}
void loop() {
// Reconnect if connection dropped
if (!client.connected()) {
connectAWS();
}
client.loop();
// Publish every PUBLISH_INTERVAL ms
if (millis() - lastPublish > PUBLISH_INTERVAL) {
publishSensorData();
lastPublish = millis();
}
}
Step 7 — Upload and Test
Select your ESP32 board in Arduino IDE (Tools → Board → ESP32 Dev Module), choose the correct COM port, and click Upload. Open the Serial Monitor at 115200 baud. You should see:
[WiFi] Connecting to YourSSID............
[WiFi] Connected. IP: 192.168.1.105
[AWS] Connecting to IoT Core
[AWS] Connected!
[AWS] Subscribed to: $aws/things/ESP32_Device_01/shadow/update/delta
[MQTT] Published: {"state":{"reported":{"temperature":"22.8","humidity":"61.2","wifi_rssi":-54,"uptime_s":12}}}
Step 8 — Verify in AWS Console
In AWS IoT Core, go to Test → MQTT test client. Subscribe to the topic # (wildcard). Within 10 seconds you should see the JSON payload from your ESP32 appear in the console.
To test Device Shadow control, publish the following to $aws/things/ESP32_Device_01/shadow/update:
{
"state": {
"desired": {
"led": true
}
}
}
The ESP32 will receive this via the /shadow/update/delta topic and turn its onboard LED on.
desired (what the cloud wants) and reported (what the device actually has) states. When they differ, AWS publishes a delta message to your device automatically.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| rc=-2 (connection refused) | Wrong endpoint or port | Double-check endpoint from IoT Core Settings |
| rc=-4 (connection timeout) | Port 8883 blocked by firewall | Try network without firewall or use port 443 |
| TLS handshake failed | Wrong or mismatched certificate | Ensure all three certs are correct and activated |
| Policy denied | Policy not attached to certificate | Attach policy in IoT Core → Security → Certificates |
| Publish fails | Payload too large | Increase buffer: client.setBufferSize(2048) |
Next Steps
Now that your ESP32 is connected securely to AWS IoT Core, here's what to build next:
- AWS Lambda Rules Engine — trigger serverless functions when sensor values exceed a threshold
- DynamoDB time-series storage — store all sensor readings for historical dashboards
- Amazon SNS alerts — send SMS or email when temperature goes above a setpoint
- OTA Updates — deliver firmware updates over the air using AWS IoT Jobs
If you found this guide helpful, consider sharing it with your team. For custom IoT development or consultancy, reach out to ESPSTACK.