Implement Real‑Time Web Message Push (Red Dot) Using Polling, SSE, WebSocket & MQTT
This article walks through multiple ways to add real‑time in‑site notifications—such as the familiar red‑dot badge—to a web application, covering short and long polling, iframe streaming, Server‑Sent Events, MQTT and WebSocket, with complete Spring Boot code examples and client‑side snippets.
The author explains how to add an in‑site message push (the red‑dot notification) to a web application, presenting several implementation schemes and providing complete sample code.
Before implementation, the requirements are simple: when an event occurs (e.g., a resource is shared or the backend pushes a message), the web page’s notification badge should increment in real time.
What is push?
Push notifications are messages sent proactively from a server to a user's web page or mobile app to attract attention. They can be classified as web‑side push or mobile‑side push.
Web‑side push is commonly used for site messages, unread email counts, monitoring alerts, etc.
Short polling
Short polling repeatedly sends an HTTP request at a fixed interval (e.g., every second) to fetch the unread count.
<code>setInterval(() => {
// request method
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data;
}
});
}, 1000);
</code>While simple, short polling generates unnecessary traffic because the client requests even when no new data exists.
Long polling
Long polling keeps the request open until the server has new data, reducing wasted requests. It is widely used in configuration centers (Nacos, Apollo) and message queues (Kafka, RocketMQ).
<code>@Controller
@RequestMapping("/polling")
public class PollingController {
// store long‑polling requests per ID
public static Multimap<String, DeferredResult<String>> watchRequests =
Multimaps.synchronizedMultimap(HashMultimap.create());
@GetMapping("watch/{id}")
@ResponseBody
public DeferredResult<String> watch(@PathVariable String id) {
DeferredResult<String> dr = new DeferredResult<>(TIME_OUT);
dr.onCompletion(() -> watchRequests.remove(id, dr));
watchRequests.put(id, dr);
return dr;
}
@GetMapping("publish/{id}")
@ResponseBody
public String publish(@PathVariable String id) {
if (watchRequests.containsKey(id)) {
for (DeferredResult<String> dr : watchRequests.get(id)) {
dr.setResult("Update " + new Date());
}
}
return "success";
}
}
</code>If the request times out, an AsyncRequestTimeoutException is thrown and can be handled globally:
<code>@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String handle(AsyncRequestTimeoutException e) {
System.out.println("Async request timeout");
return "304";
}
}
</code>Iframe streaming
By inserting a hidden <iframe> whose src points to a streaming endpoint, the server continuously writes HTML/JS to the iframe, achieving real‑time updates.
<code><iframe src="/iframe/message" style="display:none"></iframe>
</code>The server writes a script repeatedly to the response:
<code>@Controller
@RequestMapping("/iframe")
public class IframeController {
@GetMapping("message")
public void message(HttpServletResponse response) throws IOException, InterruptedException {
while (true) {
response.setHeader("Pragma", "no-cache");
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache,no-store");
response.setStatus(HttpServletResponse.SC_OK);
response.getWriter().print("<script>parent.document.getElementById('count').innerHTML='" + count.get() + "';</script>");
}
}
}
</code>Because the browser shows a constantly loading iframe, this method is generally discouraged.
Server‑Sent Events (SSE)
SSE uses a single‑direction HTTP connection that streams text/event-stream data from server to client. It is simpler than WebSocket, works over standard HTTP, and supports automatic reconnection.
<code>let source = null;
if (window.EventSource) {
source = new EventSource('http://localhost:7777/sse/sub/' + userId);
source.addEventListener('open', () => console.log('Connection opened'));
source.addEventListener('message', e => setMessageInnerHTML(e.data));
} else {
console.log('Your browser does not support SSE');
}
</code>On the server side, a SseEmitter is stored in a map and used to push messages:
<code>private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId) {
try {
SseEmitter emitter = new SseEmitter(0L); // never timeout
emitter.onCompletion(() -> sseEmitterMap.remove(userId));
emitter.onError(e -> sseEmitterMap.remove(userId));
emitter.onTimeout(() -> sseEmitterMap.remove(userId));
sseEmitterMap.put(userId, emitter);
return emitter;
} catch (Exception e) { log.info("Create SSE error", e); }
return null;
}
public static void sendMessage(String userId, String msg) {
if (sseEmitterMap.containsKey(userId)) {
try { sseEmitterMap.get(userId).send(msg); }
catch (IOException e) { log.error("Push error", e); sseEmitterMap.remove(userId); }
}
}
</code>SSE is not supported by IE but works well on modern browsers.
MQTT
MQTT (Message Queue Telemetry Transport) is a lightweight publish/subscribe protocol built on TCP/IP, ideal for IoT scenarios where bandwidth is limited and devices need asynchronous messaging.
It separates publishers from subscribers, allowing reliable delivery even on unstable networks, and can broadcast commands to many devices simultaneously.
WebSocket
WebSocket establishes a full‑duplex TCP connection, enabling bidirectional communication after a single handshake.
<code>@Component
@Slf4j
@ServerEndpoint("/websocket/{userId}")
public class WebSocketServer {
private Session session;
private static final CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>();
private static final Map<String, Session> sessionPool = new HashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
webSockets.add(this);
sessionPool.put(userId, session);
log.info("New connection, total: " + webSockets.size());
}
@OnMessage
public void onMessage(String message) {
log.info("Received from client: " + message);
}
public void sendOneMessage(String userId, String msg) {
Session s = sessionPool.get(userId);
if (s != null && s.isOpen()) {
s.getAsyncRemote().sendText(msg);
}
}
}
</code> <code><script>
var ws = new WebSocket('ws://localhost:7777/websocket/10086');
ws.onopen = function() { console.log('ws open'); ws.send('test1'); };
ws.onmessage = function(e) { console.log('msg from server:', e.data); ws.close(); };
ws.onerror = function(err) { console.log(err); };
</script>
</code>The client opens the connection once and can send/receive messages bidirectionally.
Custom push strategy
In practice, the choice of push technology depends on the specific business scenario. Third‑party services (e.g., GoEasy, Jiguang) are convenient, but large enterprises often build their own platforms that integrate SMS, email, WeChat, and other channels.
Building a robust push system involves content moderation, audience targeting, rate limiting, failure compensation, and handling high‑concurrency data streams.
GitHub repository
All the sample code discussed in the article is available in the author’s GitHub repository. Feel free to star the project if you find it useful.
Sanyou's Java Diary
Passionate about technology, though not great at solving problems; eager to share, never tire of learning!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.