Home > IoT > How to Remotely Access ESP32 Microcontroller over the Internet

How to Remotely Access ESP32 Microcontroller over the Internet

Author: Ganesh Velrajan

Last Updated: Jul 28, 2026

The ESP32 microcontroller is one of the most widely deployed chips in IoT — dual-core Xtensa CPU, built-in Wi-Fi and Bluetooth, and enough processing power for sensor aggregation, motor control, and edge inference. Getting remote access to a deployed ESP32 device is a common and legitimate operational need: reading live sensor values, pushing configuration changes, sending commands, or delivering firmware updates — all without a site visit.

The challenge is architectural. The ESP32 runs FreeRTOS, not Linux. It has no Linux userspace, no SSH daemon, no package manager, and no ability to run a Linux binary like the SocketXP agent. Tutorials that tell you to sudo systemctl enable ssh on an ESP32 are describing a different class of hardware entirely.

This article describes the correct architecture for ESP32 remote access using SocketXP: a Linux gateway on the same network runs the SocketXP agent and provides the remote access channel, while the ESP32 communicates to the gateway over local Wi-Fi using protocols it natively supports (MQTT, HTTP, UART).


Architecture Overview

                    Remote Operator / Developer
                              │
                              │  SocketXP Cloud Gateway
                              │  (SSL/TLS tunnel)
                              │
                    ┌─────────▼──────────┐
                    │   Linux Gateway    │
                    │  (Raspberry Pi,    │
                    │  Jetson, x86 PC,   │
                    │  or any Linux SBC) │
                    │                    │
                    │  SocketXP agent    │
                    │  Mosquitto broker  │
                    │  Bridge service    │
                    └─────────┬──────────┘
                              │ local Wi-Fi (MQTT / HTTP)
                              │ or UART/SPI/I²C (wired)
                    ┌─────────▼──────────┐
                    │  ESP32 (FreeRTOS)  │
                    │  Sensor / actuator │
                    │  mbedTLS + paho    │
                    └────────────────────┘

The Linux gateway — a Raspberry Pi, NVIDIA Jetson, industrial x86 gateway, or any Linux single-board computer on the same local network — runs the SocketXP agent. It is the device that has a secure outbound tunnel to the SocketXP Cloud Gateway and is remotely accessible from anywhere.

The ESP32 connects to the Linux gateway over local Wi-Fi using MQTT or HTTP — protocols that are natively supported by ESP-IDF’s built-in libraries. It publishes sensor data to the gateway’s MQTT broker and subscribes to command topics. It does not connect directly to the internet for management purposes.

This architecture is correct, practical, and maps to the way most production ESP32 deployments are actually structured: an ESP32 handles real-time sensing and actuation at the edge; a more capable Linux companion device handles communication, storage, and management connectivity.


What You Need

  • An ESP32 development board (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, or ESP32-C6) with ESP-IDF v5.x
  • A Linux gateway device on the same local network (Raspberry Pi, NVIDIA Jetson, or any Debian/Ubuntu Linux device)
  • A valid SocketXP account and auth token (from the SocketXP portal)
  • The SocketXP agent installed on the Linux gateway (download)

Step 1: Set Up the Linux Gateway with SocketXP

All SocketXP setup happens on the Linux gateway, not on the ESP32.

Install the SocketXP Agent

On the Linux gateway, download and install the SocketXP agent:

curl -O https://portal.socketxp.com/download/linux/socketxp
chmod +x socketxp
sudo mv socketxp /usr/local/bin/

Register the Gateway with SocketXP

socketxp login 

Expose the Gateway’s SSH Service

socketxp connect tcp://localhost:22

The gateway now appears in the SocketXP portal under Devices. You can SSH into it from anywhere using the SocketXP web terminal or IoT Slave Mode from your development machine.

Install Mosquitto MQTT Broker on the Gateway

The ESP32 will publish its sensor data to a Mosquitto broker running locally on the gateway. Install it:

sudo apt-get install -y mosquitto mosquitto-clients
sudo systemctl enable mosquitto
sudo systemctl start mosquitto

Mosquitto listens on port 1883 by default (local network only). Since the broker is only reachable on the local network — not exposed to the internet — this is suitable for most deployments.


Step 2: Configure the ESP32 to Publish Data to the Gateway

On the ESP32 side, use ESP-IDF’s MQTT client to connect to the Mosquitto broker on the Linux gateway and publish sensor readings.

ESP-IDF MQTT Client Configuration

#include "mqtt_client.h"
#include "esp_log.h"
#include 

static const char *TAG = "esp32_gateway";

/* Replace with your Linux gateway's local IP address */
#define GATEWAY_IP   "192.168.1.100"
#define MQTT_PORT    1883
#define DEVICE_ID    "esp32-sensor-001"

static void mqtt_event_handler(void *arg, esp_event_base_t base,
                                int32_t event_id, void *event_data)
{
    esp_mqtt_event_handle_t event = event_data;
    switch (event->event_id) {
        case MQTT_EVENT_CONNECTED:
            ESP_LOGI(TAG, "Connected to gateway MQTT broker");
            /* Subscribe to command topic so the gateway can send instructions */
            esp_mqtt_client_subscribe(event->client,
                                      "commands/" DEVICE_ID "/#", 1);
            break;
        case MQTT_EVENT_DATA:
            ESP_LOGI(TAG, "Command received: topic=%.*s payload=%.*s",
                     event->topic_len, event->topic,
                     event->data_len,  event->data);
            /* Handle incoming command from the operator via the gateway */
            handle_remote_command(event->topic, event->topic_len,
                                   event->data,  event->data_len);
            break;
        default: break;
    }
}

void start_mqtt_and_publish(void)
{
    esp_mqtt_client_config_t cfg = {
        .broker.address.uri = "mqtt://" GATEWAY_IP ":" XSTR(MQTT_PORT),
    };

    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&cfg);
    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID,
                                    mqtt_event_handler, NULL);
    esp_mqtt_client_start(client);

    /* Publish a sensor reading every 10 seconds */
    while (1) {
        float temperature = read_temperature_sensor();
        float humidity    = read_humidity_sensor();

        char payload[128];
        snprintf(payload, sizeof(payload),
                 "{\"device\":\"%s\",\"temp\":%.1f,\"humidity\":%.1f}",
                 DEVICE_ID, temperature, humidity);

        esp_mqtt_client_publish(client,
                                "sensors/" DEVICE_ID "/data",
                                payload, 0, 1, 0);

        ESP_LOGI(TAG, "Published: %s", payload);
        vTaskDelay(pdMS_TO_TICKS(10000));
    }
}

The ESP32 connects to the local Mosquitto broker on the gateway (not to the internet directly), publishes its sensor data, and subscribes to a command topic so it can receive instructions from a remote operator.


Step 3: Access ESP32 Data Remotely via the Gateway

With the gateway accessible through SocketXP and the ESP32 publishing to Mosquitto on the gateway, a remote operator can subscribe to live sensor data in two ways.

Method A: SSH into the Gateway and Subscribe

# From the SocketXP portal web terminal, or via IoT Slave Mode on your laptop:
# SSH into the Linux gateway, then subscribe to all ESP32 sensor topics

mosquitto_sub -h localhost -p 1883 -t "sensors/esp32-sensor-001/#" -v
# Output:
# sensors/esp32-sensor-001/data {"device":"esp32-sensor-001","temp":24.3,"humidity":58.1}
# sensors/esp32-sensor-001/data {"device":"esp32-sensor-001","temp":24.5,"humidity":57.9}

Method B: Expose a Data Dashboard via SocketXP HTTP Tunnel

Run a lightweight web dashboard on the gateway that displays ESP32 data:

# On the gateway — start a Python-based MQTT-to-HTTP bridge (example)
python3 -m pip install flask paho-mqtt
python3 esp32_dashboard.py &   # Listens on port 8080

# Expose the dashboard through SocketXP
socketxp connect http://localhost:8080
# Returns a public HTTPS URL for the dashboard

The dashboard receives data from Mosquitto via MQTT and serves it as a web page accessible through the SocketXP HTTPS tunnel — from any browser, anywhere.


Step 4: Send Remote Commands to the ESP32

Operators can send commands to the ESP32 by publishing to its command topic from the gateway’s SSH session:

# SSH into the gateway via SocketXP, then publish a command to the ESP32

# Turn on an LED
mosquitto_pub -h localhost -p 1883 \
  -t "commands/esp32-sensor-001/led" \
  -m '{"action":"on","color":"red"}'

# Change the sensor sampling interval to 5 seconds
mosquitto_pub -h localhost -p 1883 \
  -t "commands/esp32-sensor-001/config" \
  -m '{"sampling_interval_ms":5000}'

# Trigger an immediate sensor reading
mosquitto_pub -h localhost -p 1883 \
  -t "commands/esp32-sensor-001/read" \
  -m '{"immediate":true}'

The ESP32, which subscribes to commands/esp32-sensor-001/#, receives these payloads and executes the corresponding action in its handle_remote_command() function. The round-trip is: operator → SocketXP tunnel → gateway SSH → mosquitto_pub → local MQTT → ESP32.


Step 4b: Interactive Console Access to the ESP32 via Telnet from the Gateway

MQTT command delivery is well-suited for scripted, automated operations. For interactive debugging sessions — querying live state, changing parameters on the fly, or troubleshooting a specific device — a telnet console on the ESP32 gives you a direct interactive prompt accessible from the Linux gateway over the local Wi-Fi link.

The ESP32 runs a lightweight TCP server on port 23. The Linux gateway connects using telnet or nc. Because this connection stays on the local network between the gateway and the ESP32, it is never exposed to the internet.

ESP32 Telnet Console Server (ESP-IDF)

Add the following to your ESP32 application. The telnet task starts after Wi-Fi is connected:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "lwip/sockets.h"
#include "esp_log.h"
#include 
#include 

#define TELNET_PORT    23
#define CMD_BUF_SIZE   256

static const char *TAG = "esp32_telnet";

/* Register the commands your console understands */
static void dispatch_command(int sock, const char *cmd)
{
    char response[256];

    if (strcmp(cmd, "status") == 0) {
        snprintf(response, sizeof(response),
                 "temp=%.1f  humidity=%.1f  uptime=%lus\r\n",
                 read_temperature_sensor(),
                 read_humidity_sensor(),
                 (unsigned long)(esp_timer_get_time() / 1000000));

    } else if (strncmp(cmd, "interval ", 9) == 0) {
        int ms = atoi(cmd + 9);
        set_sampling_interval_ms(ms);
        snprintf(response, sizeof(response),
                 "Sampling interval set to %d ms\r\n", ms);

    } else if (strcmp(cmd, "reset") == 0) {
        send(sock, "Restarting...\r\n", 15, 0);
        vTaskDelay(pdMS_TO_TICKS(100));
        esp_restart();

    } else if (strcmp(cmd, "help") == 0) {
        snprintf(response, sizeof(response),
                 "Commands:\r\n"
                 "  status          — print current sensor readings and uptime\r\n"
                 "  interval    — set sampling interval in milliseconds\r\n"
                 "  reset           — restart the device\r\n"
                 "  quit            — close this telnet session\r\n");

    } else if (strcmp(cmd, "quit") == 0 || strcmp(cmd, "exit") == 0) {
        send(sock, "Bye.\r\n", 6, 0);
        close(sock);
        vTaskDelete(NULL);

    } else {
        snprintf(response, sizeof(response),
                 "Unknown command: %s  (type 'help')\r\n", cmd);
    }

    send(sock, response, strlen(response), 0);
}

static void telnet_client_task(void *arg)
{
    int sock = (int)(intptr_t)arg;
    char buf[CMD_BUF_SIZE];
    int  pos = 0;

    const char *banner =
        "\r\n=== ESP32 Remote Console ===\r\n"
        "Type 'help' for available commands.\r\n> ";
    send(sock, banner, strlen(banner), 0);

    int n;
    while ((n = recv(sock, buf + pos, 1, 0)) > 0) {
        char c = buf[pos];

        /* Echo the character back so the terminal displays it */
        send(sock, &c, 1, 0);

        if (c == '\n' || c == '\r') {
            send(sock, "\r\n", 2, 0);
            buf[pos] = '\0';

            /* Strip carriage return if line ended with \r\n */
            if (pos > 0 && buf[pos - 1] == '\r') buf[--pos] = '\0';

            if (pos > 0) dispatch_command(sock, buf);
            pos = 0;
            send(sock, "> ", 2, 0);

        } else if (c == 127 || c == '\b') {
            /* Backspace */
            if (pos > 0) { pos--; send(sock, "\b \b", 3, 0); }

        } else if (pos < CMD_BUF_SIZE - 2) {
            pos++;
        }
    }

    close(sock);
    ESP_LOGI(TAG, "Telnet client disconnected");
    vTaskDelete(NULL);
}

void start_telnet_server(void)
{
    struct sockaddr_in addr = {
        .sin_family      = AF_INET,
        .sin_addr.s_addr = htonl(INADDR_ANY),
        .sin_port        = htons(TELNET_PORT),
    };

    int server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    int opt = 1;
    setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    bind(server,   (struct sockaddr *)&addr, sizeof(addr));
    listen(server, 3);

    ESP_LOGI(TAG, "Telnet server ready on port %d", TELNET_PORT);

    while (1) {
        struct sockaddr_in client_addr;
        socklen_t len = sizeof(client_addr);
        int client = accept(server, (struct sockaddr *)&client_addr, &len);

        char ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, ip, sizeof(ip));
        ESP_LOGI(TAG, "Telnet connection from %s", ip);

        /* Each client gets its own small task — 4 KB stack is sufficient */
        xTaskCreate(telnet_client_task, "telnet_client",
                    4096, (void *)(intptr_t)client, 5, NULL);
    }
}

Call start_telnet_server() from app_main() after Wi-Fi is connected, running it in its own FreeRTOS task:

xTaskCreate(start_telnet_server_task, "telnet_srv", 4096, NULL, 4, NULL);

Connecting from the Linux Gateway

After SSHing into the Linux gateway via the SocketXP portal or IoT Slave Mode, connect to the ESP32’s telnet console using the ESP32’s local IP address:

# Find the ESP32's local IP (check your router's DHCP table, or
# read it from the ESP32 serial output during boot)

# Connect using telnet
telnet 192.168.1.50 23

# Or using netcat if telnet is not installed on the gateway
nc 192.168.1.50 23

You will see the ESP32 console banner immediately:

=== ESP32 Remote Console ===
Type 'help' for available commands.
> help
Commands:
  status          — print current sensor readings and uptime
  interval    — set sampling interval in milliseconds
  reset           — restart the device
  quit            — close this telnet session

> status
temp=24.3  humidity=58.1  uptime=3721s

> interval 2000
Sampling interval set to 2000 ms

> quit
Bye.

How This Fits in the Remote Access Flow

The full path from the remote operator to the ESP32 console is:

Remote operator
    │
    │  SocketXP tunnel (SSL/TLS)
    ▼
Linux gateway  ──── telnet 192.168.1.50 23 ────► ESP32 port 23
 (SSH session)          local Wi-Fi                 (FreeRTOS telnet task)

The telnet traffic travels over the local Wi-Fi segment between gateway and ESP32. It never reaches the internet. The operator’s connection to the gateway is secured by SocketXP’s SSL/TLS tunnel — the telnet leg is protected by the private local network.

Important Caveats

  • Telnet is unencrypted on the wire. The data between the Linux gateway and the ESP32 travels as plaintext over local Wi-Fi. This is acceptable when both devices are on a trusted private network (home or office Wi-Fi, dedicated IoT VLAN), but is not appropriate on shared or public Wi-Fi segments.
  • One active session at a time is the practical limit for the simple implementation above. The task-per-client model allows multiple connections, but the ESP32’s limited RAM means you should configure your firewall or application logic to restrict concurrent sessions.
  • Not for high-throughput data. The telnet console is designed for interactive command exchange, not for streaming large amounts of sensor data. Use MQTT for data pipelines.

Step 5: Remote OTA Firmware Updates to the ESP32

Delivering a new firmware binary to a deployed ESP32 without a site visit requires two stages: getting the firmware onto the gateway, and getting it from the gateway onto the ESP32.

Stage 1: Deliver Firmware to the Gateway via SocketXP OTA

Use SocketXP’s OTA file delivery to push the compiled ESP32 firmware binary to the Linux gateway:

# From the SocketXP portal, upload the firmware binary to the gateway device
# The gateway stores it at a known path, e.g.:
# /opt/esp32-firmware/esp32-sensor-app.bin

Stage 2: Flash the ESP32 from the Gateway

If the ESP32 is connected to the gateway via USB/UART (common during development or for devices near the gateway):

# On the gateway — flash the ESP32 over UART using esptool.py
pip3 install esptool

esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 921600 \
  write_flash -z 0x1000 /opt/esp32-firmware/esp32-sensor-app.bin

If the ESP32 is remote from the gateway but connected over Wi-Fi, use ESP-IDF’s built-in HTTP OTA mechanism. The gateway hosts the firmware file on a local HTTP server, and the ESP32 downloads and self-flashes:

# On the gateway — serve the firmware over HTTP (after SocketXP OTA delivers it)
cd /opt/esp32-firmware
python3 -m http.server 8070 &

# The ESP32 then fetches it from http://192.168.1.100:8070/esp32-sensor-app.bin

ESP32 OTA code (triggered by a remote command via MQTT):

#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_log.h"

#define GATEWAY_OTA_URL "http://192.168.1.100:8070/esp32-sensor-app.bin"

void perform_ota_update(void)
{
    ESP_LOGI("OTA", "Starting OTA from gateway: %s", GATEWAY_OTA_URL);

    esp_http_client_config_t http_cfg = {
        .url = GATEWAY_OTA_URL,
    };

    esp_https_ota_config_t ota_cfg = {
        .http_config = &http_cfg,
    };

    esp_err_t ret = esp_https_ota(&ota_cfg);
    if (ret == ESP_OK) {
        ESP_LOGI("OTA", "Firmware update complete — restarting");
        esp_restart();
    } else {
        ESP_LOGE("OTA", "OTA failed: %s", esp_err_to_name(ret));
    }
}

The operator triggers OTA by publishing to the ESP32’s command topic from the gateway:

mosquitto_pub -h localhost -p 1883 \
  -t "commands/esp32-sensor-001/ota" \
  -m '{"url":"http://192.168.1.100:8070/esp32-sensor-app.bin"}'

The ESP32 receives the command, calls perform_ota_update(), downloads the new firmware from the gateway’s local HTTP server, validates it, writes it to the OTA partition, and reboots — all without any direct internet exposure of the ESP32 itself.


Step 6: Persistent SocketXP Gateway Configuration

For production deployments, configure the SocketXP agent as a systemd service on the gateway so it reconnects automatically on boot and network interruptions:

# /etc/socketxp/config.json on the Linux gateway
{
    "tunnels": [
        {
            "destination": "tcp://127.0.0.1:22",
            "custom_domain": "",
            "name": "esp32-gateway-ssh"
        },
        {
            "destination": "http://127.0.0.1:8080",
            "custom_domain": "",
            "name": "esp32-data-dashboard"
        }
    ]
}
sudo socketxp service install --config /etc/socketxp/config.json
sudo systemctl enable socketxp
sudo systemctl start socketxp

Why the Linux Gateway Architecture Is the Right Design

ConcernDirect internet ESP32Linux gateway + SocketXP
ESP32 exposes ports to internetYes — high attack surfaceNo — ESP32 never has inbound internet connections
Firmware update deliveryCustom cloud OTA service requiredSocketXP OTA to gateway + local HTTP to ESP32
Sensor data accessRequires cloud MQTT broker subscriptionSSH to gateway + local mosquitto_sub
Remote command deliveryRequires cloud message queueGateway SSH + local mosquitto_pub
Authentication at the edgePer-device cloud credentials requiredLocal MQTT over private network; no internet-facing credentials on ESP32
Agent binary compatibilityESP32 (FreeRTOS) cannot run Linux binariesGateway runs the Linux SocketXP agent
Network requirement on ESP32Must reach cloud endpointsMust reach local gateway IP only

The gateway architecture also makes the system more resilient: the ESP32 continues collecting and buffering sensor data even when the internet connection is down, and syncs to the gateway when connectivity resumes.


Frequently Asked Questions

Q: Can I access an ESP32 that has no Linux companion device? A: If the ESP32 is the only device at the deployment site, it must connect directly to a cloud MQTT broker or HTTPS backend for data. Remote management — commands, OTA — then requires a cloud-side MQTT topic delivery or an HTTPS endpoint the ESP32 polls. SocketXP is not involved in that path because the ESP32 cannot run the SocketXP agent binary. A Linux gateway on the same network is the recommended approach for SocketXP-based remote access.

Q: What is the minimum Linux gateway I can use? A: Any Linux device that can run the SocketXP agent binary works: Raspberry Pi Zero 2W (512MB RAM), Orange Pi, BeagleBone, or any x86/ARM64 Linux SBC. The SocketXP agent is a small statically-linked binary with no heavy dependencies.

Q: Can the gateway support multiple ESP32 devices? A: Yes. Mosquitto handles hundreds of concurrent MQTT connections on even a Raspberry Pi 4. Each ESP32 uses a unique device ID in its topic paths (sensors/esp32-sensor-001/, sensors/esp32-sensor-002/). The gateway bridges all of them over a single SocketXP tunnel.

Q: Can SocketXP push firmware to the ESP32 directly? A: Not directly — SocketXP’s OTA delivery targets the Linux gateway. The gateway then uses esptool.py (for UART-connected ESP32s) or hosts a local HTTP server for ESP32’s built-in OTA mechanism (for Wi-Fi-connected ESP32s). The two-stage delivery is covered in Step 5 above.

Q: Does this architecture work if the ESP32 and gateway are on different networks? A: They must be on the same local network (or at minimum the ESP32 must be able to reach the gateway’s IP). If they are on different networks, the ESP32 would need to connect to a cloud MQTT broker directly rather than to the local gateway broker.


Next Steps

SocketXP IoT Remote Access and Device Management Platform

Remotely access, manage, and update your IoT & AIoT edge fleet with SocketXP's secure and scalable platform.

Start Your Free Trial Now!

Join thousands of satisfied users who trust SocketXP for a secure, reliable, and scalable IoT Edge device management solution. Start your free trial now.