ESP32 HTTP Client the Easy Way & Low Memory Footprint
A high-performance, fluent, and object-oriented HTTP client for ESP32 with direct JSON binding capabilities.
Official Documentation Get started quickly with the Quick Start Guide or explore the API Reference for detailed usage.
- Why this library?
- Performance & Comparison
- Key Features
- Installation
- Quick Start
- Initialization
- Usage
- Examples
- API Reference
Writing HTTP requests on embedded systems shouldn't feel like a chore. The standard approach forces you to manage connection states, handle string buffers manually, and allocate large chunks of RAM just to parse a simple JSON response.
ESP32-HTTP-Client acts as a bridge between your variables and your API. You don't "parse" JSON, you tell the client where to put the data.
A typical request with the Arduino SDK looks like this:
- Initialize
HTTPClient. - Make the request.
- Check error codes.
- Call
http.getString(), allocating a largeStringon the heap. - Create a
DynamicJsonDocument, allocating even more RAM. - Call
deserializeJson(). - Extract values manually.
- Hope you didn't run out of heap.
// One line. Zero intermediate strings. Direct memory binding.
client.get("/sensor").getBody("temperature", &myFloatVariable);The following data is the result of a benchmark running 100 consecutive HTTP GET requests with JSON payloads on an ESP32 using the public JSONPlaceholder /users endpoint as the test source.
JSONPlaceholder /users endpoint
| Metric / Feature | Standard (HTTPClient + ArduinoJson) | ESP32-HTTP-Client | Comparison |
|---|---|---|---|
| Memory Usage (Heap per req) | ~58.2 KB | ~0.0 KB (15 bytes) | ⬇ ~99.9% less RAM per request |
| Avg. RAM Footprint (Estimate) | 34.2% | 24.3% | ⬇ ~29% less overall RAM used |
| Absolute Min. Free Heap | 114.3 KB | 128.6 KB | ⬆ Safer for large applications |
| Execution Time (Average) | ~750 ms | ~59 ms | 🚀 ~12x faster (Native Keep-Alive) |
| Code Verbosity | High (~15 lines of boilerplate) | Low (1 fluent chain) | ⬇ Clean & maintainable code |
| JSON Parsing | Requires deserializeJson() |
Automatic, direct binding | ⬆ No JSON document allocation |
Note
Execution Time & Keep-Alive: Because ESP32-HTTP-Client safely reuses the underlying TLS connection and parses the response directly from the network stream (with native Transfer-Encoding: chunked decoding), it avoids the massive penalty of repeatedly establishing TLS handshakes. This makes it over 10x faster than the traditional approach while keeping the memory footprint exceptionally low.
- Fluent chaining — build requests naturally:
.get().query().getBody(). - Direct injection — JSON values are written straight into standard C types (
int,float,bool,char*) or C++structs. - Zero buffering — the response stream is parsed in place; the full payload is never stored.
- Struct <-> JSON mapping — direct bidirectional struct serialization/deserialization without dynamic document allocations.
- Full REST support —
GET,POST,PUT,PATCH, andDELETEare all first-class citizens. - IoT ready — designed for connecting ESP32 devices to cloud backends, Firebase, AWS API Gateway, or custom servers.
Add ESP32-HTTP-Client to the lib_deps section of your platformio.ini:
lib_deps =
PedroFnseca/ESP32-HTTP-Client@^1.4.0- Open Arduino IDE and go to Sketch → Include Library → Manage Libraries....
- Search for
ESP32-HTTP-Client. - Click Install.
#include <WiFi.h>
#include "ESP32HTTPClient.h"
ESP32HTTPClient client("https://jsonplaceholder.typicode.com");
void setup() {
Serial.begin(115200);
WiFi.begin("SSID", "PASS");
while (WiFi.status() != WL_CONNECTED) delay(100);
int userId = 0;
// API returns: { "userId": 1, "id": 1, "title": "..." }
client.get("/todos/1").getBody("userId", &userId);
Serial.printf("User ID fetched from API: %d\n", userId);
}
void loop() {}ESP32HTTPClient client("https://api.example.com");Specify the port as the second argument if your API runs on a non-standard port.
ESP32HTTPClient client("http://my-local-server.local", 8080);// Produces: GET /users?page=2&limit=20&search=pedro
client.get("/users")
.query("page", 2)
.query("limit", 20)
.query("search", "pedro");Replace {placeholder} segments dynamically in the request path:
// Produces: GET /users/15
client.get("/users/{id}")
.path("id", 15);int newId;
// Body: { "name": "Pedro", "role": "admin", "age": 21 }
client.post("/users")
.body("name", "Pedro")
.body("role", "admin")
.body("age", 21)
.getBody("id", &newId);Use dot notation to navigate nested objects.
char val[32];
// Response: { "level0": { "level1": "val2" } }
client.get("/nested")
.getBody("level0.level1", val, sizeof(val));Use a numeric index as a path segment to address array elements.
char city[32];
// Response: [ { "address": { "city": "Gwenborough" } }, { "address": { "city": "Wisokyburgh" } } ]
client.get("/users")
.getBody("1.address.city", city, sizeof(city)); // resolves the second elementBind to an Arduino String to capture an entire object or sub-array for manual processing.
String entireArray;
String specificUser;
client.get("/users")
.getBody("", &entireArray) // captures the root-level array
.getBody("1", &specificUser); // captures the second user objectWarning
Pulling complete objects or arrays into an Arduino String causes dynamic memory reallocation as the raw JSON is copied character by character. Avoid this pattern with large payloads, as it can fragment or exhaust the device heap.
Note
If a key is missing, misspelled, or the path does not exist in the response, the target variable is left unchanged. The library will not crash.
By default, the client automatically maintains a persistent TCP/TLS connection across requests (HTTP Keep-Alive). This drastically improves performance for subsequent requests to the same server, but it holds onto the connection memory buffers (e.g., ~45KB for an active TLS tunnel).
To manually close the connection and free this memory when you are done making requests, call end():
client.end(); // Closes the connection and frees TLS RAMEasily authenticate requests using built-in helpers for Bearer tokens, Basic Auth, or API keys:
// Bearer / JWT Token
client.bearer("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...");
// HTTP Basic Auth (automatically Base64 encoded)
client.basic("admin", "secret123");
// API Key header
client.apiKey("x-api-key", "my-secret-api-key");Direct bidirectional mapping between C++ structs and JSON payloads:
struct User {
int id = 0;
char name[32] = {0};
bool active = false;
REST_JSON_MAP(
REST_FIELD(id),
REST_FIELD(name),
REST_FIELD(active)
)
};
// Send struct as JSON body
User newUser = {1, "Pedro", true};
client.post("/users").body(newUser);
// Receive response directly into struct
User fetchedUser;
client.get("/users/1").getBody(&fetchedUser);// Update a resource
client.update("/lights/1").body("state", "OFF");
// Delete a resource
client.del("/logs/system_error.log");Runnable sketches are available in the examples/ directory:
| Sketch | Description |
|---|---|
| RestCrud | Full suite of REST CRUD operations (GET, POST, PUT, PATCH, DELETE) in a single sketch. |
| StructJson | Bidirectional C++ Struct <-> JSON serialization and HTTP transfer. |
| UrlParameters | Path parameters (/users/{id}) and query parameters (?page=2). |
| AuthHelpers | Bearer token, Basic Auth, and API Key authentication headers. |
| CallbacksAndErrors | Callbacks, timeouts, automatic retries, error handling, and runtime URL change. |
| PortSelection | Connecting to a server on a custom port. |
| NestedJSON | Extracting fields from deeply nested objects. |
| ArrayJSON | Addressing array elements by index. |
| RawArrayJSON | Capturing raw arrays or objects into Arduino Strings. |
| UnixTimestamp | Fetching the current Unix Timestamp from an API. |
The main entry point. Instantiate once with your base URL and reuse across requests.
| Signature | Description | Example |
|---|---|---|
ESP32HTTPClient(baseUrl) |
Creates a client. Port defaults to 80 (HTTP) or 443 (HTTPS). | ESP32HTTPClient client("https://api.example.com"); |
ESP32HTTPClient(baseUrl, port) |
Creates a client targeting a custom port. | ESP32HTTPClient client("http://192.168.1.100", 8080); |
Each method returns a RestRequest that can be chained with .query(), .body(), and .getBody().
| Method | Description | Example |
|---|---|---|
get(path) |
Sends a GET request to baseUrl + path. |
client.get("/todos/1") |
post(path) |
Sends a POST request to baseUrl + path. |
client.post("/users") |
put(path) |
Sends a PUT request to baseUrl + path. |
client.put("/users/1") |
update(path) |
Alias for put(). |
client.update("/lights/1") |
patch(path) |
Sends a PATCH request to baseUrl + path. |
client.patch("/config/wifi") |
del(path) |
Sends a DELETE request to baseUrl + path. |
client.del("/logs/old.log") |
| Method | Description | Example |
|---|---|---|
bearer(token) |
Sets persistent Bearer token authorization header. | client.bearer("my-token"); |
basic(user, password) |
Sets persistent Basic authorization header (auto Base64 encoded). | client.basic("admin", "123456"); |
apiKey(name, key) |
Sets persistent API key header. | client.apiKey("x-api-key", "my-key"); |
setHeader(name, value) |
Registers a custom HTTP header that is sent with every subsequent request. | client.setHeader("Authorization", "Bearer mytoken123"); |
setBaseUrl(url, port) |
Changes the base URL and target port at runtime. | client.setBaseUrl("https://api.v2.com", 443); |
setUrl(url) |
Changes the base URL at runtime. | client.setUrl("http://192.168.1.100"); |
setPort(port) |
Changes the target TCP port at runtime. | client.setPort(8080); |
setTimeout(timeoutMs) |
Sets default request timeout in milliseconds (default: 60000, 1 min). | client.setTimeout(10000); |
setMaxRetry(maxRetry) |
Sets default max retries on network failure (default: 1). | client.setMaxRetry(3); |
setContentType(contentType) |
Overrides the Content-Type header used for request bodies. Defaults to application/json. |
client.setContentType("application/x-www-form-urlencoded"); |
getStatusCode() |
Returns the HTTP status code of the last completed request. | int code = client.getStatusCode(); |
isSuccess() |
Returns true if last request status was 2xx (200 <= code < 300). |
if (client.isSuccess()) { ... } |
hasError() |
Returns true if last request had network error or HTTP error (code >= 400). |
if (client.hasError()) { ... } |
getErrorMessage() |
Returns descriptive error string for last status or error code. | String err = client.getErrorMessage(); |
onSuccess(cb) |
Registers client-level callback for successful requests (2xx). | client.onSuccess([](int code){ ... }); |
onError(cb) |
Registers client-level callback for failed requests. | client.onError([](int code, const char* msg){ ... }); |
onResponse(cb) |
Registers client-level callback executed on every completed request. | client.onResponse([](int code){ ... }); |
toJson(struct) |
Static utility to serialize a mapped struct into JSON string. | String json = ESP32HTTPClient::toJson(user); |
fromJson(json, struct) |
Static utility to populate a struct from a JSON string. | ESP32HTTPClient::fromJson(json, &user); |
end() |
Closes the persistent TCP/TLS connection and frees its memory buffers. Useful after a burst of requests. | client.end(); |
Returned by every HTTP method on ESP32HTTPClient. All builder methods return RestRequest&, enabling fluent chaining. The underlying HTTP request is dispatched on the first call to .getBody(), or automatically when the object goes out of scope.
| Method | Description | Example |
|---|---|---|
path(key, value) |
Replaces a {placeholder} in the URL path. Supports String, const char*, int, long, float, double, and bool. Chainable. |
client.get("/users/{id}").path("id", 15) |
query(key, value) |
Appends a URL query parameter. Supports String, const char*, int, long, float, double, and bool. Chainable. |
client.get("/users").query("page", 2).query("limit", 20) |
body(key, value) |
Adds a field to the JSON request body. Supports the same types as query(). Chainable. |
client.post("/users").body("name", "Pedro").body("age", 21) |
body(struct) |
Sets full JSON request body serialized from a mapped struct. Chainable. | client.post("/users").body(user) |
timeout(ms) |
Overrides timeout for this specific request in milliseconds. Chainable. | client.get("/data").timeout(2000) |
retry(maxRetry) |
Overrides max retry attempts for this specific request. Chainable. | client.get("/data").retry(3) |
onSuccess(cb) |
Per-request success callback (2xx). Chainable. | client.get("/users").onSuccess([](int c){ ... }) |
onError(cb) |
Per-request error callback (code < 200 || code >= 400). Chainable. |
client.get("/users").onError([](int c, const char* m){ ... }) |
onResponse(cb) |
Per-request callback executed on completion. Chainable. | client.get("/users").onResponse([](int c){ ... }) |
getBody() is overloaded for each supported C type. It registers a binding between a JSON key path and a target variable. Use dot notation for nested fields and numeric segments for array indices.
getHeader() is overloaded for each supported C type to extract HTTP response headers directly (e.g., token, Content-Type, Date). Header lookups are case-insensitive.
| Method | Description | Example |
|---|---|---|
getBody(key, int* target) |
Binds a JSON integer to *target. |
client.get("/data").getBody("count", &myInt) |
getBody(key, float* target) |
Binds a JSON number to a float. |
client.get("/sensor").getBody("temp", &myFloat) |
getBody(key, double* target) |
Binds a JSON number to a double. |
client.get("/sensor").getBody("voltage", &myDouble) |
getBody(key, bool* target) |
Binds a JSON boolean to *target. |
client.get("/status").getBody("active", &myBool) |
getBody(key, long* target) |
Binds a JSON integer to a long. |
client.get("/stats").getBody("timestamp", &myLong) |
getBody(key, char* target, size_t maxLen) |
Copies a JSON string into a char buffer, up to maxLen bytes. |
client.get("/user").getBody("name", myChar, sizeof(myChar)) |
getBody(key, String* target) |
Copies a raw JSON object or array into an Arduino String. Pass "" to capture the entire response. |
client.get("/users").getBody("", &entireJson) |
getBody(struct* target) |
Binds and populates a mapped struct directly from root JSON response. | client.get("/users/1").getBody(&user) |
getBody(key, struct* target) |
Binds and populates a mapped struct from nested JSON object path. | client.get("/profile").getBody("data.user", &user) |
getHeader(name, target) |
Extracts an HTTP response header into target (String*, char*/char[N], int*, long*, float*, double*, bool*). Case-insensitive. |
client.get("/auth").getHeader("token", &token) |
Note
If a key or header is missing, the target variable is left unchanged. No exception is thrown and no crash occurs.
int userId;
float temperature;
char city[32];
String token;
client.post("/report")
.body("device", "esp32-cam")
.body("floor", 3)
.timeout(3000)
.retry(2)
.onSuccess([](int code) { Serial.printf("OK: %d\n", code); })
.onError([](int code, const char* msg) { Serial.printf("Fail (%d): %s\n", code, msg); })
.getHeader("token", &token) // String — response header
.getBody("userId", &userId) // int — root field
.getBody("sensor.temp", &temperature) // float — nested object
.getBody("0.address.city", city, sizeof(city)); // char* — array index + nested| Code | Meaning | Category |
|---|---|---|
-1 |
Connection Refused | Client error |
-2 |
Send Header Failed | Client error |
-3 |
Send Payload Failed | Client error |
-4 |
Not Connected | Client error |
-5 |
Connection Lost | Client error |
-6 |
No Stream | Client error |
-7 |
No HTTP Server | Client error |
-8 |
Too Less RAM | Client error |
-9 |
Encoding Error | Client error |
-10 |
Stream Write Error | Client error |
-11 |
Read Timeout | Client error |
200 |
OK | HTTP success |
201 |
Created | HTTP success |
202 |
Accepted | HTTP success |
204 |
No Content | HTTP success |
400 |
Bad Request | HTTP client error |
401 |
Unauthorized | HTTP client error |
403 |
Forbidden | HTTP client error |
404 |
Not Found | HTTP client error |
405 |
Method Not Allowed | HTTP client error |
408 |
Request Timeout | HTTP client error |
409 |
Conflict | HTTP client error |
429 |
Too Many Requests | HTTP client error |
500 |
Internal Server Error | HTTP server error |
501 |
Not Implemented | HTTP server error |
502 |
Bad Gateway | HTTP server error |
503 |
Service Unavailable | HTTP server error |
504 |
Gateway Timeout | HTTP server error |
0 |
Not Executed | Internal state |
- Negative unknown codes → Unknown Client Error
- 200–299 → Success
- 300–399 → Redirection
- 400–499 → Client Error
- 500–599 → Server Error
- Other values → Unknown HTTP Status
A short usage example showing how applications can handle both transport errors and HTTP errors:
int status = client.get("/api/data").getStatusCode();
if (client.isSuccess()) {
// Handle successful response
} else {
Serial.println(client.getErrorMessage());
}If this library saved you time, consider leaving a star ⭐ on the repository.