What

A minimal ESP32 web server that responds with “OK” when pinged. Used to detect on-prem power/network status in cloud-failover architectures.

Use case: You have an on-prem server with cloud backup. Ping the ESP32 to know if power is up. If ping fails, start cloud backup.

Components:

  • ESP32 board
  • WiFi connection (2.4GHz)
  • ESP-IDF framework
  • HTTP server responding on port 80

Why

  • Failover detection: Cloud apps can ping this to know if on-prem is offline.
  • Cheap: ESP32 costs $5-10. No need for UPS monitoring APIs or expensive hardware.
  • Simple: Single endpoint (/) returns OK. No auth, no dependencies.
  • Low power: ESP32 draws minimal current. Runs indefinitely.
  • Reliable: No OS overhead. Boots in seconds after power restoration.

Real scenario: On-prem PostgreSQL + cloud RDS standby. ESP32 sits on same network. Cloud Lambda pings every 30 seconds. If 3 consecutive failures, promote RDS and switch DNS. When ESP32 responds again, you know on-prem is back.

How

Step 1 — Install ESP-IDF

Follow the official setup: ESP32 Development Setup on Ubuntu

Key steps:

mkdir -p ~/esp
cd ~/esp
git clone --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh
. ./export.sh

Add to ~/.profile:

. $HOME/esp/esp-idf/export.sh

Step 2 — Create Project Structure

mkdir -p ~/esp/esp32-ping-server/main
cd ~/esp/esp32-ping-server

Step 3 — Root CMakeLists.txt

Create CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(ping-server)

Step 4 — Main Component CMakeLists.txt

Create main/CMakeLists.txt:

idf_component_register(SRCS "main.c"
                    INCLUDE_DIRS ".")

Step 5 — Main Application Code

Create main/main.c:

#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "esp_http_server.h"

#define WIFI_SSID      "YOUR_WIFI_SSID"
#define WIFI_PASS      "YOUR_WIFI_PASSWORD"
#define WIFI_MAXIMUM_RETRY  5

static EventGroupHandle_t s_wifi_event_group;
#define WIFI_CONNECTED_BIT BIT0
#define WIFI_FAIL_BIT      BIT1

static const char *TAG = "ping-server";
static int s_retry_num = 0;

static void event_handler(void* arg, esp_event_base_t event_base,
                                int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
        if (s_retry_num < WIFI_MAXIMUM_RETRY) {
            esp_wifi_connect();
            s_retry_num++;
            ESP_LOGI(TAG, "Retry to connect to the AP");
        } else {
            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
        }
        ESP_LOGI(TAG,"Connect to the AP fail");
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
        ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
        ESP_LOGI(TAG, "Got IP:" IPSTR, IP2STR(&event->ip_info.ip));
        s_retry_num = 0;
        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
}

void wifi_init_sta(void)
{
    s_wifi_event_group = xEventGroupCreate();

    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
                                                        ESP_EVENT_ANY_ID,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
                                                        IP_EVENT_STA_GOT_IP,
                                                        &event_handler,
                                                        NULL,
                                                        &instance_got_ip));

    wifi_config_t wifi_config = {
        .sta = {
            .ssid = WIFI_SSID,
            .password = WIFI_PASS,
            .threshold.authmode = WIFI_AUTH_OPEN,
        },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
    ESP_ERROR_CHECK(esp_wifi_start() );

    ESP_LOGI(TAG, "wifi_init_sta finished.");

    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
            WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
            pdFALSE,
            pdFALSE,
            portMAX_DELAY);

    if (bits & WIFI_CONNECTED_BIT) {
        ESP_LOGI(TAG, "Connected to AP SSID:%s", WIFI_SSID);
    } else if (bits & WIFI_FAIL_BIT) {
        ESP_LOGI(TAG, "Failed to connect to SSID:%s", WIFI_SSID);
    } else {
        ESP_LOGE(TAG, "UNEXPECTED EVENT");
    }
}

static esp_err_t root_get_handler(httpd_req_t *req)
{
    const char* resp_str = "OK";
    httpd_resp_set_status(req, "200 OK");
    httpd_resp_set_type(req, "text/plain");
    httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

static const httpd_uri_t root = {
    .uri       = "/",
    .method    = HTTP_GET,
    .handler   = root_get_handler,
    .user_ctx  = NULL
};

static httpd_handle_t start_webserver(void)
{
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.lru_purge_enable = true;

    ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port);
    if (httpd_start(&server, &config) == ESP_OK) {
        ESP_LOGI(TAG, "Registering URI handlers");
        httpd_register_uri_handler(server, &root);
        return server;
    }

    ESP_LOGI(TAG, "Error starting server!");
    return NULL;
}

void app_main(void)
{
    ESP_ERROR_CHECK(nvs_flash_init());

    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
    wifi_init_sta();

    start_webserver();

    ESP_LOGI(TAG, "Web server started. Access via ESP32 IP address on port 80");
}

Update:

  • Line 14: Your WiFi SSID
  • Line 15: Your WiFi password

Step 6 — Build and Flash

cd ~/esp/esp32-ping-server
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor

Replace /dev/ttyUSB0 with your serial port if different.

Step 7 — Find ESP32 IP Address

In the serial monitor output:

I (xxxx) ping-server: Got IP:192.168.1.9

Why: Avoid checking logs every time. Hardcode IP in monitoring scripts.

ESP32 MAC Address: Check serial output or router DHCP table.

Router DHCP reservation:

  1. Access router admin (usually 192.168.1.1)
  2. Navigate to DHCP settings
  3. Add reservation:
    • MAC: Your ESP32 MAC address (check serial output or router DHCP table)
    • IP: 192.168.1.9 (your choice)
    • Name: ESP32-PingServer

Benefit: ESP32 always gets same IP. No log checks needed.

Step 9 — Test

curl http://192.168.1.9

Expected: OK with HTTP 200 status.

Or open http://192.168.1.9 in browser.

Integration with Cloud Failover

Example: AWS Lambda Health Check

import urllib.request
import boto3

PING_URL = "http://192.168.1.9"
TIMEOUT = 5

def lambda_handler(event, context):
    try:
        response = urllib.request.urlopen(PING_URL, timeout=TIMEOUT)
        if response.status == 200 and response.read().decode() == "OK":
            return {"status": "on-prem-online"}
    except:
        pass

    return {"status": "on-prem-offline"}

Run this Lambda every 30 seconds via EventBridge. Store status in DynamoDB. If 3 consecutive failures, trigger failover workflow.

Example: Monitoring Script (Linux)

#!/bin/bash

PING_URL="http://192.168.1.9"
FAIL_COUNT=0
MAX_FAILS=3

while true; do
    if curl -f -s -m 5 "$PING_URL" | grep -q "OK"; then
        echo "$(date): On-prem online"
        FAIL_COUNT=0
    else
        FAIL_COUNT=$((FAIL_COUNT + 1))
        echo "$(date): Ping failed ($FAIL_COUNT/$MAX_FAILS)"

        if [ $FAIL_COUNT -ge $MAX_FAILS ]; then
            echo "$(date): On-prem offline. Starting cloud failover..."
            # Trigger your failover logic here
            # Examples:
            # - Update Route53 to point to cloud endpoint
            # - Start cloud RDS instance
            # - Send SNS alert
        fi
    fi

    sleep 30
done

Troubleshooting

WiFi Connection Fails

Symptoms:

wifi:state: run -> init (0xf00)

Solutions:

  • Verify SSID and password (case-sensitive)
  • Ensure WiFi is 2.4GHz (ESP32 doesn’t support 5GHz)
  • Check router MAC filtering
  • Disable AP isolation on router

Brownout Detector Triggered

Error:

E BOD: Brownout detector was triggered

Causes: Insufficient USB power.

Solutions:

  • Connect directly to computer USB port (not hub)
  • Use USB 3.0 port (blue) - provides more current
  • Try different USB cable (data cable, not charge-only)
  • Use external 5V power supply

Can’t Access Web Server

Check:

  • Verify ESP32 got IP: idf.py -p /dev/ttyUSB0 monitor
  • Ping from same network: ping 192.168.1.9
  • Check firewall rules on testing device
  • Ensure device is on same WiFi network

Notes

  • Power consumption: ESP32 draws ~80mA when WiFi is active. Use 5V/1A supply minimum.
  • Boot time: ~2-3 seconds from power-on to server ready.
  • Reliability: No filesystem, no complex state. Just WiFi + HTTP server.
  • Security: No authentication. Only use on trusted networks or add basic auth if needed.
  • Limitations: HTTP only (not HTTPS). Fine for internal monitoring.
  • Response time: <50ms typical on local network.

Improvements

Add timestamp endpoint

static esp_err_t time_get_handler(httpd_req_t *req)
{
    char resp[64];
    snprintf(resp, sizeof(resp), "%lld", esp_timer_get_time() / 1000000);
    httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

static const httpd_uri_t time_uri = {
    .uri       = "/time",
    .method    = HTTP_GET,
    .handler   = time_get_handler,
    .user_ctx  = NULL
};

// In start_webserver():
httpd_register_uri_handler(server, &time_uri);

Add JSON response

#include "cJSON.h"

static esp_err_t status_handler(httpd_req_t *req)
{
    cJSON *root = cJSON_CreateObject();
    cJSON_AddStringToObject(root, "status", "ok");
    cJSON_AddNumberToObject(root, "uptime", esp_timer_get_time() / 1000000);

    char *json_str = cJSON_Print(root);
    httpd_resp_set_type(req, "application/json");
    httpd_resp_send(req, json_str, strlen(json_str));

    cJSON_Delete(root);
    free(json_str);
    return ESP_OK;
}

Use static IP (instead of DHCP)

// After esp_netif_create_default_wifi_sta():
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
esp_netif_dhcp_status_t status;
ESP_ERROR_CHECK(esp_netif_dhcpc_get_status(netif, &status));
if (status == ESP_NETIF_DHCP_STARTED) {
    ESP_ERROR_CHECK(esp_netif_dhcpc_stop(netif));
}

esp_netif_ip_info_t ip_info;
IP4_ADDR(&ip_info.ip, 192, 168, 1, 9);
IP4_ADDR(&ip_info.gw, 192, 168, 1, 1);
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
ESP_ERROR_CHECK(esp_netif_set_ip_info(netif, &ip_info));

Project Files

esp32-ping-server/
├── CMakeLists.txt
└── main/
    ├── CMakeLists.txt
    └── main.c

Useful Commands

# Build
idf.py build

# Flash
idf.py -p /dev/ttyUSB0 flash

# Monitor
idf.py -p /dev/ttyUSB0 monitor

# Flash and monitor
idf.py -p /dev/ttyUSB0 flash monitor

# Clean build
idf.py fullclean

# Erase flash
idf.py -p /dev/ttyUSB0 erase-flash

Summary

You’ve built a minimal ESP32 ping server for on-prem power monitoring:

  1. ✓ Connects to WiFi
  2. ✓ Runs HTTP server on port 80
  3. ✓ Responds with “OK” at /
  4. ✓ Uses ~80mA power
  5. ✓ Boots in 2-3 seconds
  6. ✓ No dependencies or complex state

Use it in:

  • Cloud failover detection
  • Network monitoring
  • Power restoration alerts
  • Simple health checks