Mastering Web Push: 6 Real‑World Strategies from Short Polling to SSE and WebSocket
This article walks through six practical web‑push techniques—including short polling, long polling, iframe streaming, Server‑Sent Events, MQTT, and WebSocket—explaining their principles, trade‑offs, and providing complete Spring Boot and JavaScript code samples to help developers implement real‑time notification badges.
What is Message Push (push)
Push notifications are proactive messages sent from a server to a web page or mobile app to attract user interaction, such as unread message counts or alerts. Push can be divided into web‑side push and mobile‑side push.
Short Polling
Short polling repeatedly sends HTTP requests at fixed intervals to fetch unread message counts. A simple JavaScript timer can request the count every second.
setInterval(() => {
// request method
messageCount().then((res) => {
if (res.code === 200) {
this.messageCount = res.data;
}
});
}, 1000);While easy to implement, 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 load while preserving near‑real‑time updates. It is widely used in middleware such as Nacos, Apollo, Kafka, and RocketMQ.
In Spring Boot we use DeferredResult (Servlet 3.0) to handle asynchronous requests. A Multimap from Guava stores pending long‑polling requests keyed by an identifier.
@Controller
@RequestMapping("/polling")
public class PollingController {
public static Multimap<String, DeferredResult<String>> watchRequests =
Multimaps.synchronizedMultimap(HashMultimap.create());
@GetMapping("watch/{id}")
@ResponseBody
public DeferredResult<String> watch(@PathVariable String id) {
DeferredResult<String> deferredResult = new DeferredResult<>(TIME_OUT);
deferredResult.onCompletion(() -> watchRequests.remove(id, deferredResult));
watchRequests.put(id, deferredResult);
return deferredResult;
}
@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";
}
}If the request exceeds the timeout, an AsyncRequestTimeoutException is thrown and handled globally with @ControllerAdvice to return a 304 status, prompting the client to start a new long‑poll.
@ControllerAdvice
public class AsyncRequestTimeoutHandler {
@ResponseStatus(HttpStatus.NOT_MODIFIED)
@ResponseBody
@ExceptionHandler(AsyncRequestTimeoutException.class)
public String asyncRequestTimeoutHandler(AsyncRequestTimeoutException e) {
System.out.println("Async request timed out");
return "304";
}
}Iframe Streaming
By embedding a hidden <iframe> whose src points to a server endpoint that continuously writes HTML/JavaScript, the page receives updates without explicit AJAX calls.
<iframe src="/iframe/message" style="display:none"></iframe>The server repeatedly writes a script that updates DOM elements.
@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>");
}
}
}Although simple, the constantly loading iframe can be visually distracting.
Server‑Sent Events (SSE)
SSE uses a single‑direction HTTP stream ( text/event-stream) to push text messages from server to client. It works on standard HTTP servers, supports automatic reconnection, and is easy to implement.
let source = null;
let userId = 7777;
if (window.EventSource) {
source = new EventSource('http://localhost:7777/sse/sub/' + userId);
source.addEventListener('open', function(e) { console.log('Connection opened'); }, false);
source.addEventListener('message', function(e) { console.log(e.data); });
} else {
console.log('Your browser does not support SSE');
}On the server side a SseEmitter is stored in a map and used to send messages to a specific user.
private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
public static SseEmitter connect(String userId) {
try {
SseEmitter emitter = new SseEmitter(0L); // never timeout
emitter.onCompletion(completionCallBack(userId));
emitter.onError(errorCallBack(userId));
emitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, emitter);
return emitter;
} catch (Exception e) { /* log */ }
return null;
}
public static void sendMessage(String userId, String message) {
if (sseEmitterMap.containsKey(userId)) {
try { sseEmitterMap.get(userId).send(message); }
catch (IOException e) { /* handle */ }
}
}MQTT
MQTT (Message Queue Telemetry Transport) is a lightweight publish/subscribe protocol built on TCP/IP, ideal for IoT scenarios where devices need asynchronous, low‑overhead messaging.
WebSocket
WebSocket establishes a full‑duplex TCP connection, allowing bidirectional communication after a single handshake.
@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 WebSocket connection, total: {}", webSockets.size());
}
@OnMessage
public void onMessage(String message) {
log.info("Received from client: {}", message);
}
public void sendOneMessage(String userId, String message) {
Session s = sessionPool.get(userId);
if (s != null && s.isOpen()) {
s.getAsyncRemote().sendText(message);
}
}
} var ws = new WebSocket('ws://localhost:7777/webSocket/10086');
ws.onopen = function() { ws.send('test1'); };
ws.onmessage = function(event) { console.log('Server says:', event.data); };
ws.onclose = function() { console.log('WebSocket closed'); };
ws.onerror = function(err) { console.error(err); };Choosing a Solution
Each method has trade‑offs: short polling is simple but wasteful; long polling reduces load but still generates many requests; iframe streaming is easy but visually noisy; SSE offers simple HTTP‑based one‑way push with auto‑reconnect; MQTT excels in IoT environments; WebSocket provides full‑duplex communication suitable for real‑time interactive apps.
In practice, select the approach that matches your system’s requirements, traffic patterns, and client capabilities.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
