An Arduino Uno or Mega with an Ethernet Shield is still one of the most reliable ways to put a sensor or relay on MQTT: no Wi-Fi to drop, no radio interference, and a wired link that just works in a cabinet or a basement. The standard stack is the Ethernet library for the WIZnet chip plus PubSubClient for MQTT.
This tutorial gets a W5100 or W5500 shield connected to a broker, publishing a sensor value and accepting commands, with a reconnect loop that survives cable pulls and broker restarts. Then you verify it end to end from the browser.
Hardware and libraries
| Part | Notes |
|---|---|
| Arduino Uno R3 or Mega 2560 | Uno: 2 KB RAM, Mega: 8 KB RAM |
| Ethernet Shield W5100 or Ethernet Shield 2 (W5500) | Talks SPI, chip select on pin 10 |
| Network cable to a router with DHCP | Or a static IP on your LAN |
Install two libraries from the Library Manager: Ethernet (by Arduino, supports W5100, W5200 and W5500) and PubSubClient (by Nick O'Leary). Most shields also carry a microSD slot whose chip select is pin 4; if no card is used, drive pin 4 high so it does not interfere with the SPI bus.
The complete sketch
#include <SPI.h>
#include <Ethernet.h>
#include <PubSubClient.h>
// Use a unique MAC per board on your network
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x10, 0x01 };
const char* MQTT_HOST = "broker.connectmq.com";
const uint16_t MQTT_PORT = 1883;
const char* CLIENT_ID = "arduino-eth-0001";
const char* MQTT_USER = "username";
const char* MQTT_PASS = "password";
const char* T_STATUS = "testmqtt/arduino-eth/0001/status";
const char* T_TEMP = "testmqtt/arduino-eth/0001/temp";
const char* T_CMD = "testmqtt/arduino-eth/0001/cmd/led";
EthernetClient ethClient;
PubSubClient mqtt(ethClient);
unsigned long lastReconnect = 0;
unsigned long lastPublish = 0;
void onMessage(char* topic, byte* payload, unsigned int length) {
Serial.print(F("<- "));
Serial.print(topic);
Serial.print(F(" = "));
Serial.write(payload, length);
Serial.println();
if (strcmp(topic, T_CMD) == 0 && length > 0) {
digitalWrite(LED_BUILTIN, payload[0] == '1' ? HIGH : LOW);
}
}
bool connectMqtt() {
Serial.print(F("MQTT connecting... "));
// id, user, pass, willTopic, willQos, willRetain, willMessage
if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS, T_STATUS, 1, true, "offline")) {
Serial.println(F("connected"));
mqtt.publish(T_STATUS, "online", true);
mqtt.subscribe(T_CMD, 1);
return true;
}
Serial.print(F("failed, rc="));
Serial.println(mqtt.state());
return false;
}
void setup() {
pinMode(LED_BUILTIN, OUTPUT);
pinMode(4, OUTPUT);
digitalWrite(4, HIGH); // deselect the SD card
Serial.begin(9600);
Ethernet.init(10); // W5100/W5500 chip select
Serial.println(F("DHCP..."));
if (Ethernet.begin(mac) == 0) {
if (Ethernet.hardwareStatus() == EthernetNoHardware) {
Serial.println(F("No Ethernet shield found"));
} else {
Serial.println(F("DHCP failed - check the cable or use a static IP"));
}
while (true) delay(1000);
}
Serial.print(F("IP: "));
Serial.println(Ethernet.localIP());
mqtt.setServer(MQTT_HOST, MQTT_PORT);
mqtt.setCallback(onMessage);
mqtt.setKeepAlive(30);
}
void loop() {
if (!mqtt.connected()) {
unsigned long now = millis();
if (now - lastReconnect > 5000) { // retry every 5 s without blocking
lastReconnect = now;
connectMqtt();
}
} else {
mqtt.loop();
}
if (mqtt.connected() && millis() - lastPublish > 10000) {
lastPublish = millis();
char buf[8];
int raw = analogRead(A0);
itoa(raw, buf, 10);
mqtt.publish(T_TEMP, buf);
}
Ethernet.maintain(); // renew the DHCP lease
}A few choices in this sketch matter more than they look:
F("...")strings keep constant text in flash instead of RAM. On an Uno, a dozen debug strings can otherwise eat a quarter of your memory.- The reconnect is non-blocking.
loop()keeps running between attempts, so local logic (buttons, relays, a watchdog) does not freeze while the broker is unreachable. mqtt.loop()must run often. It reads incoming packets and sends the keep-alive PINGREQ. A longdelay()in your code makes the broker drop the connection after 1.5 × keep alive.- Last Will and a retained
onlinegive any dashboard a reliable online/offline flag. See MQTT Last Will.
Static IP instead of DHCP
Industrial networks often have no DHCP. Pass the address, DNS server, gateway and subnet explicitly; without a DNS entry the hostname broker.connectmq.com cannot be resolved:
IPAddress ip(192, 168, 1, 50);
IPAddress dns(192, 168, 1, 1);
IPAddress gateway(192, 168, 1, 1);
IPAddress subnet(255, 255, 255, 0);
Ethernet.begin(mac, ip, dns, gateway, subnet);If DNS is unavailable, mqtt.setServer() also accepts an IPAddress, but then a change of broker IP breaks every device. Prefer a hostname whenever the network allows it.
Test it end to end
- Open the Serial Monitor at 9600 baud. You should see an IP address, then
MQTT connecting... connected. - Open the online MQTT client, connect to
broker.connectmq.comover secure WebSocket (port 8084, path/mqtt) and subscribe totestmqtt/arduino-eth/0001/#. - The retained
status = onlinearrives immediately, followed by atempvalue every ten seconds. - Publish
1totestmqtt/arduino-eth/0001/cmd/led. The on-board LED turns on and the Serial Monitor prints the message. Publish0to turn it off. - Unplug the network cable. After about 45 seconds the broker sends the will and the status changes to
offline. Plug it back in: the sketch reconnects within a few seconds and publishesonlineagain.
Watch the Arduino in the online client
If you would rather test from a terminal, mosquitto_pub and mosquitto_sub do the same job: mosquitto_sub -h broker.connectmq.com -t 'testmqtt/arduino-eth/#' -v.
Reading mqtt.state()
When connect() fails, state() tells you why. These are the codes PubSubClient defines:
| Code | Constant | Meaning |
|---|---|---|
| -4 | MQTT_CONNECTION_TIMEOUT | No response within keep alive |
| -3 | MQTT_CONNECTION_LOST | Network connection broke |
| -2 | MQTT_CONNECT_FAILED | TCP connection failed (DNS, IP, port, firewall) |
| -1 | MQTT_DISCONNECTED | Cleanly disconnected |
| 1 | MQTT_CONNECT_BAD_PROTOCOL | Broker rejected the protocol version |
| 2 | MQTT_CONNECT_BAD_CLIENT_ID | Client ID rejected |
| 3 | MQTT_CONNECT_UNAVAILABLE | Broker unavailable |
| 4 | MQTT_CONNECT_BAD_CREDENTIALS | Wrong username or password |
| 5 | MQTT_CONNECT_UNAUTHORIZED | Not authorized |
failed, rc=-2 is by far the most common: the shield never reached the broker over TCP. Check that Ethernet.localIP() is not 0.0.0.0, that the DNS server is reachable, and that outbound port 1883 is allowed. The PubSubClient troubleshooting guide walks through every code in detail.
Memory and packet size limits
PubSubClient allocates one buffer for both incoming and outgoing packets. Its default size is 256 bytes, and that includes the fixed header, the topic and the payload. A packet larger than the buffer is silently dropped on receive and publish() returns false on send.
mqtt.setBufferSize(512); // call once in setup(), before connect()On an Uno, keep the buffer modest and keep topics short. You can work out exactly how large a packet will be with the packet size calculator. On a Mega, 1–2 KB buffers are realistic. If a large payload still fails, the packet too large guide covers broker-side limits too.
Other limits to plan around:
- QoS 0 publishes only. PubSubClient publishes at QoS 0 and subscribes at QoS 0 or 1. Use a retained state topic or an application acknowledgement when delivery matters.
- MQTT 3.1.1 only. No reason codes or MQTT 5 properties.
- Socket count. The W5100 has 4 sockets and the W5500 has 8. DHCP and DNS use one briefly, so a sketch that also runs a web server can run out.
When to move beyond the Uno
The Uno + Ethernet combination is perfect for simple, wired, local devices. If you need TLS, larger JSON payloads or MQTT 5 features, the same PubSubClient code ports almost unchanged to an ESP32 with a W5500 or to Wi-Fi boards: see ESP32 with Arduino and MQTT. If you want QoS 1 publishing on AVR, the arduino-mqtt library is worth a look.
A wired Arduino on MQTT is mostly about discipline: unique MACs and client IDs, F() strings, a non-blocking reconnect and a loop that never stalls. Get the serial log to say connected, prove publish, command and Last Will from the online client, and the board will run untouched for years.
Frequently asked questions
Can an Arduino Uno use MQTT over TLS?
Not in practice. The ATmega328P has 2 KB of RAM, far too little for a TLS handshake, and the W5100/W5500 chips do not do TLS themselves. Use plain MQTT on a trusted network, a local gateway that forwards over TLS, or move to an ESP32 or a board with more memory.
Why does PubSubClient not receive my longer messages?
PubSubClient drops any packet larger than its buffer, which defaults to 256 bytes including the topic and headers. Call setBufferSize() with a larger value, keeping in mind that on an Uno every byte comes out of the 2 KB of RAM.
Does PubSubClient support QoS 1 publishing?
No. PubSubClient publishes at QoS 0 only and supports MQTT 3.1.1. It can subscribe with QoS 0 or 1. If you need QoS 1 publishes, add an application-level acknowledgement or use a different library.

