Course-এর সূচি

2.5

Real-Time ১: Short Polling, Long Polling ও SSE

কিছু বদলালে server কীভাবে browser-কে জানাবে, তার তিনটা উপায় আর প্রতিটার খরচ।

45 মিনিট পড়া + Animation

টিপস: ডটেড-আন্ডারলাইন করা যেকোনো শব্দে ট্যাপ বা Hover করলে তার অর্থ দেখবে।

এই Lesson শেষে তুমি পারবে

  • বোঝাতে, কেন সাধারণ HTTP নিজে থেকে Push করতে পারে না, আর তবুও কিছু বদলালে Server কোন কোন উপায়ে Client-কে জানাতে পারে: Short Polling, Long Polling, Server-Sent Events, WebSocket, WebRTC, Webhook আর Mobile Push।
  • Short Polling-এর খরচ হিসাব করতে (প্রতি সেকেন্ডে কত Request, কত ভাগ উত্তর খালি, গড়ে আর সবচেয়ে খারাপ ক্ষেত্রে কত দেরি), আর Conditional Request, Back-off আর Cache দিয়ে সেটা সস্তা করতে।
  • ঠিকভাবে Long Polling বানাতে: পথের সব Timeout-এর চেয়ে কম সময় ধরে রাখা, Cursor যাতে কোনো Event ফাঁকে না পড়ে, একবারে একটাই Poll, আর Jitter সহ Retry।
  • text/event-stream Format পড়তে আর লিখতে (data, event, id, retry, Comment), আর EventSource ব্যবহার করতে, Last-Event-ID দিয়ে নিজে নিজে আবার Connect হওয়া সহ।
  • Production-এ SSE চালাতে: Proxy Buffering, Compression, Idle Timeout আর Heartbeat, HTTP/1.1-এর ছয় Connection-এর সীমা, নিজের Header ছাড়াই Authentication, Deploy আর Reconnect-এর ঝড়।
  • বোঝাতে, LLM Chat API কেন SSE দিয়ে Token Stream করে; আর Browser Tab-এর বাইরের Push হিসেবে Webhook (Retry, Signature, Idempotency) আর Mobile Push (APNs, FCM) Design করতে।
  • সংখ্যা দিয়ে তিনটার মধ্যে একটা বেছে নিতে, আর Design Interview-তে সেই পছন্দের পক্ষে যুক্তি দিতে।

#এই Lesson কোথায় বসে

Lesson ১.১-এ আমরা Web-এর মূল নিয়মটা দেখেছি: Client চায়, Server উত্তর দেয়। Server কখনো আগে কথা বলে না। একটা Page Load করার জন্য এতে কোনো সমস্যা নেই। সমস্যা হয় Page খোলা থাকা অবস্থায় যা বদলায়, সেগুলোর বেলায়: একটা Chat Message, Rider তোমার খাবার তুলে নিল, ক্রিকেটের Score, Build-এর Log, AI-এর উত্তরের পরের শব্দগুলো।

সাধারণ Request-Response-এর বাইরে গিয়ে Client-কে সবসময় হালনাগাদ রাখতে Web Application মূলত পাঁচটা কৌশল ব্যবহার করে:

#কৌশলএক লাইনে মূল ভাবনাকোন দিকেLesson
১Short Pollingকয়েক সেকেন্ড পরপর জিজ্ঞেস করা, “নতুন কিছু আছে?”Client টেনে আনেএটাই
২Long Pollingজিজ্ঞেস করো, আর খবর না আসা পর্যন্ত Server উত্তরটা ধরে রাখেClient টানে, Server উত্তর দেয় খবর এলেএটাই
৩Server-Sent Events (SSE)এমন একটা HTTP Response যা কখনো শেষ হয় না; Server তাতে Event লিখতেই থাকেServer → Clientএটাই
৪WebSocketএকটা দীর্ঘস্থায়ী Connection, যেখানে দুই পাশই যখন খুশি পাঠায়দুই দিকেইLesson ২.৬
৫WebRTCBrowser-রা একে অপরকে সরাসরি Media আর Data পাঠায়Peer থেকে PeerLesson ২.৬

এই Lesson-এ থাকছে প্রথম তিনটা, যেগুলো সাধারণ HTTP দিয়েই চলে। সাথে থাকছে Browser Tab-এর বাইরের দুটো Push-এর পথ: Webhook (Server থেকে Server) আর Mobile Push (APNs আর FCM)। WebSocket আর WebRTC আসবে Lesson ২.৬-এ; সেখানে পাঁচটাকে পাশাপাশি তুলনা করা হবে, আর Pub/Sub দিয়ে লাখো খোলা Connection কীভাবে Scale করে, সেটাও দেখানো হবে।

আগের কয়েকটা Lesson-এর ওপর ভর দেব: Keep-alive Connection আর Idle Timeout (১.১০), চুপচাপ Mapping ভুলে যাওয়া NAT (১.৭), Reverse Proxy, Buffering আর proxy_read_timeout (১.১৩), Stateful Connection আর Sticky Session (১.৩), আর ETag ও 304 Not Modified (২.৪)।

#সমস্যাটা: HTTP শুধু উত্তর দেয়

কিছু ঘটলে Server সোজা Browser-কে একটা Message পাঠিয়ে দেয় না কেন?

  • HTTP হলো Request-Response। Response দেওয়া যায় শুধু কোনো Request-এর উত্তর হিসেবে। অপেক্ষায় কোনো Request না থাকলে উত্তর দেওয়ার কিছুই নেই।
  • Browser-এর কাছে পৌঁছানো যায় না। সে বসে আছে বাসার NAT, Carrier-grade NAT, Firewall আর কয়েক মিনিট পরপর বদলানো Wi-Fi-এর পেছনে (Lesson ১.৭)। তার Public Address নেই, কোনো Port-এ সে Listen-ও করে না। প্রতিটা Connection খুলতে হয় Client থেকে বাইরের দিকে।
  • তাই প্রতিটা কৌশলই আসলে একটা কায়দা: খবর আসার মুহূর্তে যেন Server-এ একটা Request অপেক্ষায় থাকে, বা Client-এর খোলা একটা Connection থাকে। তফাত শুধু এখানে: Client কত ঘন ঘন চায়, একটা Request বা Connection কতক্ষণ খোলা থাকে, আর সেটা ভাঙলে কী হয়।

যেকোনো Push Design-কে তিনটা প্রশ্নের উত্তর দিতে হয়, আর প্রতিটা অংশে তুমি এগুলো দেখবে:

  1. Latency: ঘটনা ঘটা থেকে ব্যবহারকারীর চোখে পড়া পর্যন্ত কত সময়?
  2. খরচ: প্রতি Client-এ আর পুরো বহরে কত Request, কত খোলা Connection, কত Byte আর Server-এর কত Memory লাগে?
  3. নির্ভরযোগ্যতা: Client বিচ্ছিন্ন থাকার সময়ের Event-গুলোর কী হয়, আর সেগুলো কি দুবার বা আগে-পরে আসতে পারে?

#Short Polling

Short Polling সবচেয়ে পুরনো আর সবচেয়ে সহজ কৌশল। Client একটা Timer ধরে (ধরো প্রতি ৩ বা ৫ সেকেন্ডে) সাধারণ একটা HTTP Request পাঠিয়ে জানতে চায় নতুন কিছু আছে কি না। খবর থাকুক বা না থাকুক, Server সাথে সাথে উত্তর দেয়।

Client                                  Server
  |--- GET /orders/42/status ----------->|
  |<-- 200 "preparing" (নতুন কিছু নেই) ---|
  |        (৫ সেকেন্ড অপেক্ষা)            |
  |--- GET /orders/42/status ----------->|
  |<-- 200 "picked up" (খবর!) -----------|

Code-এ এটা fetch-এর চারপাশে একটা Loop। নোটে আছে setInterval(); তবে setTimeout() পরপর জুড়ে দেওয়া বেশি নিরাপদ, কারণ তাতে আগের Poll শেষ হলেই কেবল পরেরটা শুরু হয়। setInterval হলে Server ধীর হয়ে গেলে Request জমে জমে একটার ওপর আরেকটা চলতে থাকে।

let etag = null;
async function poll() {
  try {
    const res = await fetch('/orders/42/status', {
      headers: etag ? { 'If-None-Match': etag } : {},
    });
    if (res.status === 200) {
      etag = res.headers.get('ETag');
      render(await res.json());
    } // 304 Not Modified: কিছু বদলায়নি, কিছু করার নেই
  } finally {
    const base = document.hidden ? 60_000 : 5_000; // লুকোনো Tab-এ ধীরে চলো
    setTimeout(poll, base + Math.random() * 1_000); // Jitter: সব Client যেন একই তালে Poll না করে
  }
}
poll();

সুবিধা। বানানো আর বোঝা খুবই সহজ। আলাদা কোনো Protocol ছাড়াই যেকোনো Browser, যেকোনো HTTP Server, যেকোনো Proxy, CDN আর Firewall-এ চলে। প্রতিটা Request স্বাধীন আর Stateless, তাই যেকোনো Server উত্তর দিতে পারে, Load Balancing-এ বিশেষ কিছু লাগে না।

অসুবিধা।

  • Server-এর ওপর বেশি চাপ। বেশিরভাগ উত্তরই খালি। নোট বলছে, ৯৯% পর্যন্ত Response-এ কিছুই থাকতে পারে, ফলে Server-এর CPU আর Bandwidth প্রচুর নষ্ট হয়।
  • Latency। এটা সত্যিকারের Real-time না। Interval ৫ সেকেন্ড হলে খবর ৫ সেকেন্ড পর্যন্ত বসে থাকতে পারে।
  • Header-এর বোঝা। প্রতিটা Poll-এ দুই দিকে পুরো HTTP Header-এর বহর যায়-আসে: Cookie, Auth Token, User Agent। প্রতি Round Trip-এ প্রায়ই ৫০০–১,০০০ Byte, যা “কিছু বদলায়নি” উত্তরের চেয়ে অনেক বড়। HTTP/2-এর Header Compression কিছুটা বাঁচায়, তবু প্রতিটা Poll পুরো একটা Request, যেটা Server-কে যাচাই আর Route করতে হয়।

#সংখ্যায় Short Polling

ঘটনা ঘটে এলোমেলো মুহূর্তে, তাই পরের Poll তুলে নেওয়ার আগে একটা Event গড়ে Interval-এর অর্ধেক সময় অপেক্ষা করে, আর সবচেয়ে খারাপ ক্ষেত্রে পুরো একটা Interval (সাথে এক Round Trip)। পুরো বহরের খরচ সাধারণ ভাগ অঙ্ক:

প্রতি সেকেন্ডে Request = Client-এর সংখ্যা ÷ Interval

নোটের সতর্কবার্তাটাই হিসাব করে দেখি:

  1. ১০ লাখ খোলা Order Page, প্রতি ৫ সেকেন্ডে Poll: ১০,০০,০০০ ÷ ৫ = প্রতি সেকেন্ডে ২ লাখ Request, কিছু ঘটুক বা না ঘটুক।
  2. প্রতিটা Order মোটামুটি ১০ মিনিটে (৬০০ সেকেন্ডে) একবার বদলায়। তাহলে ৬০০ ÷ ৫ = ১২০টা Poll-এর মধ্যে মোটে একটা খবর পায়, মানে প্রায় ৯৯.২% উত্তরই খালি।
  3. প্রতি Poll-এ প্রায় ৮০০ Byte Header ধরলে: ২,০০,০০০ × ৮০০ B = সেকেন্ডে ১৬০ MB, প্রায় ১.৩ Gbps Traffic, শুধু “কিছু বদলায়নি” বলার জন্য।
  4. গড় দেরি: ২.৫ সেকেন্ড; সবচেয়ে বেশি: প্রায় ৫ সেকেন্ড।

Interval অর্ধেক করলে দেরি অর্ধেক হয়, আর ওপরের প্রতিটা খরচ দ্বিগুণ। Short Polling-এর পুরো গল্পটা এই টানাটানিতেই।

Short Polling: প্রতি ৫ সেকেন্ডে “নতুন কিছু?”

Browser একটা Timer ধরে জিজ্ঞেস করে, আর Server সাথে সাথে উত্তর দেয়, খবর থাকুক বা না থাকুক। দেখো কতগুলো উত্তর খালি, আর খবর কতক্ষণ বসে থাকে।

পরিস্থিতি

Order Page প্রতি ৫ সেকেন্ডে Poll করে। Rider খাবার তোলে t = ৭ সেকেন্ডে।

BrowserOrder Page
Serverapi.example.com
GET /orders/42/status · t=0 s
ধাপ 1 / 7
1

HTTP-তে কথা শুরু করতে পারে শুধু Client, তাই Page নিজেই জিজ্ঞেস করে। এটা সাধারণ একটা GET, সাথে চিরচেনা সব Header আর Cookie।

#Short Polling সস্তা করার উপায়

  • Conditional Request। তোমার কাছে যে Version আছে, সেটা If-None-Match-এ পাঠাও (বা If-Modified-Since-এ একটা সময়)। কিছু না বদলালে Server Body ছাড়া 304 Not Modified দেয় (Lesson ২.৪)। এতে Bandwidth আর Serialisation বাঁচে, কিন্তু Request নিজে, TLS-এর কাজ বা Auth যাচাই বাঁচে না। আর Version মেলানোটা সস্তা হতে হবে (Memory বা Redis-এ একটা Counter, Database Query না)।
  • সবার একই উত্তর CDN-এ Cache করো। অনেকে মিলে যদি একই জিনিস Poll করে, যেমন খেলার Score, তাহলে তাতে Cache-Control: max-age=2 দাও। প্রায় সব Poll-এর উত্তর CDN-ই দেবে; দর্শক যত লাখই হোক, তোমার Origin প্রতিটা Edge Location থেকে দুই সেকেন্ডে মোটামুটি একটা Request দেখবে। লাখো Screen-এ হুবহু একই Data দেখানোর সবচেয়ে সস্তা উপায়গুলোর একটা হলো Short Polling আর CDN একসাথে।
  • অবস্থা বুঝে Interval। কিছু ঘটতে থাকলে (Order রাস্তায়) দ্রুত Poll করো, কিছু না ঘটলে (রান্না হচ্ছে) ধীরে, আর Tab লুকোনো থাকলে (document.hidden) থামাও বা অনেক ধীর করো। Server চাইলে Retry-After Header বা Response-এর কোনো Field দিয়ে বলতে পারে, “৩০ সেকেন্ড পরে আবার জিজ্ঞেস কোরো”।
  • Jitter। সব Client যদি একই সময়সূচিতে Poll করে (Deploy-এর পরে সব Page একসাথে Reload হলে যেমন হয়), চাপ আসে একসাথে দমকে দমকে। এলোমেলো একটু সরিয়ে দিলে সেটা ছড়িয়ে যায়।

#কখন Short Polling-ই ঠিক

যখন Update কালেভদ্রে আসে, কয়েক সেকেন্ড দেরি চলে, আর Client-এর সংখ্যা মাঝারি: “আমার Export তৈরি হলো?”, Payment Status Page, Admin Dashboard-এর একটা Status Widget, CI Build-এর Badge। আর যখন Data সবার জন্য একই, CDN-এর পেছনে রাখা যায়। এসব ক্ষেত্রে এর সরলতাই সবার চেয়ে এগিয়ে। “Real-time লাগবে” বলা অনেক Production System আসলে ১০ সেকেন্ডের একটা Poll-এই দিব্যি চলে।

#Long Polling

Short Polling-এর অপচয় কমাতে, অথচ সাধারণ HTTP রেখেই, এসেছে Long Polling (একে Hanging GET-ও বলে; একসময় যাকে Comet বলা হতো, এটা তার অংশ)। সাধারণ Request-Response মডেল দিয়েই এটা প্রায় Real-time অভিজ্ঞতা দেয়।

Client                                  Server
  |--- GET /updates?after=41 ----------->|  (Server Request খোলা রেখে দেয়...)
  |                                      |  (Event 42 ঘটল!)
  |<-- 200 [42] -------------------------|  (Response গেল, Request শেষ)
  |--- GET /updates?after=42 ----------->|  (সাথে সাথে আবার চায়; Server আবার ধরে রাখে...)

ধাপে ধাপে কীভাবে চলে:

  1. Client একটা HTTP Request পাঠায়।
  2. Server সাথে সাথে উত্তর দেয় না। Request-টা খোলা (Hold) রেখে অপেক্ষা করে।
  3. নতুন Data আসামাত্র, বা একটা Timeout পার হলে, Server Response পাঠায়, আর ওই Request শেষ হয়।
  4. Client সাথে সাথে নতুন আরেকটা Long Poll Request পাঠায়, আর চক্র চলতে থাকে।

সুবিধা। Short Polling-এর চেয়ে অনেক কম Request, Server-এর বৃথা খাটুনিও অনেক কম। খবর তৈরি হওয়ামাত্র পৌঁছায়, তাই Latency কম (মোটামুটি এক Network যাত্রা)। পুরোটাই সাধারণ HTTP, তাই প্রায় সব Proxy আর Firewall পেরিয়ে যায়।

অসুবিধা।

  • Request/Response-এর চক্র বারবার ঘোরে। নোটে একে বলা হয়েছে ঘন ঘন HTTP Connection খোলা আর বন্ধ করার ঝামেলা। বাস্তবে Client একটাই Keep-alive TCP Connection Poll-এর পর Poll ব্যবহার করে, তাই প্রতিবার নতুন TCP বা TLS Handshake হয় না। আসল খরচ হলো প্রতি Event বা Timeout-এ পুরো একটা Request আর Response (Header সহ), আর পুরো সময়টা Server-এ খোলা পড়ে থাকা একটা Request।
  • Message-এর ক্রম আর ফাঁক। Event দ্রুত পরপর এলে, বা Client Retry করলে, উত্তর আগে-পরে আসতে পারে, দুবার আসতে পারে, বা দুই Poll-এর মাঝের Event হারিয়ে যেতে পারে। এর জন্য Cursor লাগে (নিচে দেখো)।

যেখানে Client একটা Program, Browser না, সেখানে Long Polling এখনো বেশ চালু: AWS SQS-এর ReceiveMessage, WaitTimeSeconds সর্বোচ্চ ২০ সেকেন্ড; Telegram Bot API-এর getUpdates, সাথে timeout; আর Service Discovery-র “Blocking Query”। Web-এর শুরুর দিকের Chat (যেমন ২০০৮ সালের Facebook Chat) এর ওপরই চলত। আজ এটা মূলত সেসব Library-র বিকল্প পথ, যারা WebSocket বা SSE চায় কিন্তু যেকোনো Network-এ চলতেই হবে (Socket.IO শুরু করে Long Polling দিয়ে, তারপর Upgrade করে; SignalR আগে WebSocket, তারপর SSE, শেষে Long Polling চেষ্টা করে)।

#Timeout: পথের সবচেয়ে ছোট Timer-এর চেয়ে কম সময় ধরে রাখো

একটা Request চিরকাল ধরে রাখা যায় না। পথের প্রতিটা যন্ত্র বেশিক্ষণ চুপচাপ থাকা Connection বন্ধ করে দেয়:

পথে কেসাধারণ Idle বা Read Timeout
Nginx Reverse Proxy (proxy_read_timeout)ডিফল্ট ৬০ সেকেন্ড
AWS Application Load Balancerডিফল্ট ৬০ সেকেন্ড (বদলানো যায়)
Cloudflare Proxyপ্রায় ১০০ সেকেন্ড, তারপর 524 Error
অফিসের Proxy, Mobile Carrier-এর NAT৩০ সেকেন্ড থেকে কয়েক মিনিট, যেকোনো কিছু

এগুলোর সবচেয়ে ছোটটার চেয়ে বেশি সময় Server একটা Poll ধরে রাখলে Client উত্তরের বদলে পায় 504 Gateway Timeout বা একটা Reset। তাই Server ধরে রাখে ২৫–৩০ সেকেন্ড, তারপর খালি একটা 204 No Content (বা খালি List সহ 200) দিয়ে উত্তর দেয়, আর Client আবার চায়। Client-এর নিজের Timeout Server-এর ধরে রাখার সময়ের চেয়ে বেশি হতে হবে, নইলে সফল হতে যাওয়া Poll-ও সে ছেড়ে দেবে আর পাশাপাশি দ্বিতীয় একটা শুরু করবে।

এখন প্রতি Client-এর Request = Event + Timeout। চুপচাপ একটা Client মিনিটে মোটামুটি দুটো Request খরচ করে, ৫ সেকেন্ডের Short Polling-এ যেখানে বারোটা; আর কয়েক সেকেন্ড পরপর Event পাওয়া Client প্রতি Event-এ মোটামুটি একটা Request পাঠায়।

#দুই Poll-এর মাঝের ফাঁক, আর যে Cursor সেটা সারায়

Server উত্তর দেওয়ার পরে পরের Request আসতে একটু সময় লাগে (কয়েক Millisecond, ধীর Phone-এ কয়েক সেকেন্ডও)। ওই ফাঁকে কোনো Event ঘটলে তার চড়ার মতো কোনো Request নেই। Server যদি শুধু “Live” Event অপেক্ষার Request-গুলোকে দেয়, ওই Event চুপচাপ হারিয়ে যায়।

সমাধান হলো একটা Cursor:

  • Server প্রতিটা Event-কে একটা বাড়তে থাকা ID দেয়, আর সাম্প্রতিক Event-গুলো একটা ছোট Log-এ রাখে (Memory-তে একটা Ring Buffer, একটা Redis Stream, বা Database-এর একটা Table)।
  • Client শেষ যে ID Process করেছে, সেটা পাঠায়: GET /updates?after=42।
  • Log-এ এর চেয়ে নতুন কিছু থাকলে Server সাথে সাথে ৪২-এর পরের সব কিছু, ক্রমমতো, একসাথে পাঠিয়ে দেয়। না থাকলে Request ধরে রাখে।
  • Client Event বসায় ID-এর ক্রমে, আর আগে দেখা ID বাদ দেয়। এতে দুটো Poll আড়াআড়ি হলেও (যেমন পুরনো Poll খোলা থাকতেই একটা Retry চলে গেলে) Duplicate আর আগে-পরের সমস্যা মিটে যায়। প্রতি Client-এর একবারে একটাই Poll চালু রাখো।

Log কতক্ষণ রাখবে, সেটা Product-এর সিদ্ধান্ত: সাধারণ একটা বিচ্ছিন্নতা (কয়েক মিনিট) ঢাকার মতো যথেষ্ট, আর তার চেয়ে বেশি পরে ফেরা Client-এর জন্য একটা “অনেক পুরনো, সব আবার Load করো” উত্তর।

Long Polling: Server Request ধরে রাখে

খবর আসা বা Timeout হওয়া পর্যন্ত Request Server-এ অপেক্ষা করে, তারপর Client সাথে সাথে আবার চায়। Cursor (after=…) না থাকলে দুই Request-এর ফাঁকে Message হারিয়ে যায়।

পরিস্থিতি

নোটের “Hanging GET”, শুরু থেকেই Cursor সহ।

BrowserOrder Page
Serverসর্বোচ্চ ৩০ s ধরে রাখে
GET /updates?after=41
ধাপ 1 / 5
1

Client চায় ৪১ নম্বর Event-এর পরের সবকিছু; ৪১ হলো সে শেষ যেটা দেখেছে।

#ধরে রাখা Request-এর খরচ Server-এর কাছে কত

অপেক্ষার প্রতিটা Poll মানে একটা খোলা Connection আর সামান্য State। Event-loop Server-এ (Nginx, Node.js, Go, Virtual Thread সহ Java, Python asyncio) এর দাম কয়েক KB Memory, তাই একটা মেশিন দশ হাজার থেকে কয়েক লাখ পর্যন্ত ধরে রাখতে পারে। প্রতি Request-এ একটা Thread দেওয়া Server-এ (Apache prefork, ২০০ Thread-এর পুরনো ধাঁচের Servlet Pool) অপেক্ষার প্রতিটা Poll পুরো একটা Thread দখল করে, আর ২০১তম Client অপেক্ষা শুরু করার আগেই একটা Thread-এর জন্য লাইনে দাঁড়ায়। এটাই Lesson ১.১৩-এর C10K সমস্যা: Long Polling-এর জন্য Asynchronous Server লাগে।

Design-এর আরও দুটো কথা:

  • Event আর অপেক্ষার Request প্রায়ই আলাদা Server-এ থাকে। Server A-র Order Service জানল Rider খাবার তুলেছে; অথচ Client-এর Poll অপেক্ষা করছে Server B-তে। Server-রা একটা Pub/Sub Channel দিয়ে Event ভাগাভাগি করে (Redis Pub/Sub, একটা Message Broker), যাতে যে Server Request ধরে আছে, সে-ই উত্তর দিতে পারে। Lesson ২.৬-এ WebSocket-এর জন্য এটা বিস্তারিত আছে; ধাঁচটা একই।
  • Back-off আর Jitter সহ Retry। Network Error বা 5xx হলে আবার Poll করার আগে এলোমেলো, বাড়তে থাকা সময় অপেক্ষা করো। যে Client টানা Loop-এ Retry করে, সে Server-এর ছোট্ট একটা হোঁচটকে বন্যা বানিয়ে দেয়।

#Server-Sent Events (SSE)

Server-Sent Events একটা একমুখী (Unidirectional) পথ: শুধু Server পাঠায়, একটানা, Client-এর দিকে। এটা HTML Standard-এর অংশ, চলে সাধারণ HTTP-তে: Client একটা সাধারণ GET পাঠায়, আর Server এমন একটা Response দেয় যা কখনো শেষই হয় না। যখনই খবর আসে, Server সেই Response-এর ভেতরে ছোট আরেকটা Text-এর টুকরো লিখে দেয়।

Client                                       Server
  |--- GET /orders/42/stream ------------------>|
  |    Accept: text/event-stream                |
  |<-- 200 OK ----------------------------------|
  |    Content-Type: text/event-stream          |
  |    Cache-Control: no-cache                  |
  |<=== event 101 ==============================|   (Response খোলাই থাকে)
  |<=== event 102 ==============================|
  |<=== event 103 ==============================|

কীভাবে কাজ করে:

  • Client Browser-এর EventSource API ব্যবহার করে, যেটা একটা সাধারণ HTTP GET পাঠায়।
  • Request-এ থাকে Accept: text/event-stream। এটা শুধু একটা ইঙ্গিত (EventSource নিজেই পাঠায়); Stream হওয়ার আসল শর্ত হলো Server উত্তরে দেবে Content-Type: text/event-stream।
  • Server Response খোলা রাখে, আর প্রতিটা Event ঘটার সাথে সাথে Stream করে। নোটে এখানে Connection: keep-alive-এর কথা আছে; কিন্তু HTTP/1.1-এ Persistent Connection এমনিতেই ডিফল্ট, আর HTTP/2 ও HTTP/3-এ ওই Header পুরোপুরি নিষিদ্ধ, তাই SSE চলে ওটার জোরে না। আসল কথা হলো, Server সাথে সাথে Header পাঠিয়ে দেয় আর Body কখনো শেষ করে না।

#তারে Format-টা কেমন

Body হলো UTF-8 Text, যাতে Event-গুলো আলাদা হয় একটা ফাঁকা লাইন দিয়ে। প্রতিটা লাইন field: value:

retry: 5000

id: 101
event: status
data: {"order":42,"status":"cooking"}

: এই লাইনটা Comment, Heartbeat হিসেবে কাজে লাগে

id: 102
event: status
data: {"order":42,"status":"picked_up"}

data: সাধারণ একটা Message-এর প্রথম লাইন
data: একই Message-এর দ্বিতীয় লাইন

Fieldমানে
data:আসল Payload। একটা Event-এ কয়েকটা data: লাইন থাকলে সেগুলো Newline দিয়ে জোড়া লাগে। data ছাড়া Event পৌঁছানো হয় না
event:Event-এর ধরন। না থাকলে ধরন message (ধরে onmessage); থাকলে addEventListener('status', …) সেটা পায়
id:Event-এর ID। Browser শেষটা মনে রাখে, আর আবার Connect করার সময় Last-Event-ID হিসেবে ফেরত পাঠায়
retry:Connection কাটলে আবার Connect করার আগে Browser কত Millisecond অপেক্ষা করবে
: …Comment, Browser উপেক্ষা করে। চুপচাপ Connection বাঁচিয়ে রাখতে Heartbeat হিসেবে ব্যবহার হয়

Format-টা ইচ্ছে করেই একেবারে সাদামাটা: যেকোনো ভাষার print দিয়েই বানানো যায়, Terminal-এ curl দিয়ে পড়া যায়, আর Browser-এর Network Panel-এ Debug করা যায় (Chrome প্রতিটা Event দেখায় একটা EventStream Tab-এ)।

#Client: EventSource

const es = new EventSource('/orders/42/stream'); // অন্য Origin-এ Cookie পাঠাতে { withCredentials: true }

es.addEventListener('status', (e) => render(JSON.parse(e.data), e.lastEventId));
es.onmessage = (e) => console.log('ধরন ছাড়া Event:', e.data);
es.onerror = () => {
  // readyState 0 = নিজে নিজে আবার Connect হচ্ছে; 2 = চিরতরে বন্ধ (200 ছাড়া অন্য উত্তর পেয়েছে)
  if (es.readyState === EventSource.CLOSED) setTimeout(reopen, 1_000 + Math.random() * 10_000);
};
// Page-এর আর Update না লাগলে es.close()

#Server

যে Server Response বন্ধ না করে তাতে লিখে যেতে পারে, সে-ই SSE দিতে পারে। Node.js-এ পুরো একটা, ID, আবার পাঠানো (Replay) আর Heartbeat সহ:

import http from 'node:http';

const log = []; // সাম্প্রতিক Event, আবার Connect হলে Replay করার জন্য
const clients = new Set();
let nextId = 1;
const frame = (ev) => `id: ${ev.id}\nevent: status\ndata: ${ev.data}\n\n`;

function publish(data) {
  const ev = { id: nextId++, data: JSON.stringify(data) };
  log.push(ev);
  if (log.length > 1000) log.shift();
  for (const res of clients) res.write(frame(ev));
}

http
  .createServer((req, res) => {
    if (req.url !== '/stream') return res.writeHead(404).end();
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'X-Accel-Buffering': 'no', // Nginx-কে বলো এই Response Buffer না করতে
    });
    res.write('retry: 5000\n\n');
    const last = Number(req.headers['last-event-id'] ?? 0);
    for (const ev of log) if (ev.id > last) res.write(frame(ev)); // যা মিস হয়েছে, আবার পাঠাও
    clients.add(res);
    const beat = setInterval(() => res.write(': ping\n\n'), 20_000);
    req.on('close', () => {
      clearInterval(beat);
      clients.delete(res);
    });
  })
  .listen(8080);

setInterval(() => publish({ status: 'tick', at: Date.now() }), 3_000);

#বৈশিষ্ট্য: নিজে নিজে আবার Connect, আর Event ID

হাতে বানানো Stream-এর তুলনায় SSE এগুলো বিনা খাটুনিতে দেয়:

  • নিজে নিজে আবার Connect (Automatic Reconnection)। Network Error-এ Connection ভাঙলে Browser retry-এর সময় পার করে নিজেই আবার Connect করে (ডিফল্ট কয়েক সেকেন্ড, Browser ঠিক করে)।
  • Built-in Event ID। আবার Connect করার সময় Browser শেষ পাওয়া ID-টা Last-Event-ID-এ পাঠায়, যাতে Server মিস হওয়া সব পাঠাতে পারে। Browser শুধু ID মনে রাখে; ইতিহাস রাখতে হয় Server-কে, আর কতদূর পেছন থেকে Replay করতে পারবে, সেটাও সে-ই ঠিক করে। যে Server কখনো id: লাইন পাঠায় না, সে কোনো Resume-ই পায় না; বিকল্প হলো প্রতিটা Connection-এর প্রথম Event হিসেবে পুরো একটা Snapshot (Order-এর এখনকার অবস্থা) পাঠানো।
  • Text Data। SSE বানানো হয়েছে UTF-8 Text আর JSON পাঠানোর জন্য।

দুটো নিয়ম সহজেই চোখ এড়ায়। প্রথমত, Response যদি text/event-stream সহ 200 না হয় (Deploy-এর সময় 502 বা 503, একটা 401, একটা HTML Error Page), EventSource Connection ব্যর্থ ঘোষণা করে আর কখনো আবার চেষ্টা করে না; তোমার onerror Handler-কে এলোমেলো একটু দেরি করে নতুন একটা বানাতে হবে। দ্বিতীয়ত, Server ইচ্ছে করে Stream শেষ করতে পারে 204 No Content দিয়ে, তাতে Browser থেমে যায়।

Server-Sent Events: যে Response কখনো শেষ হয় না

EventSource একটা GET খোলে; Server সেই Response-এর ভেতরে ছোট ছোট Text Event লিখতেই থাকে। id Field আছে বলেই Connection কেটে গেলেও ফাঁক ছাড়া আবার শুরু করা যায়।

পরিস্থিতি

Order Page একবার Subscribe করে, তারপর প্রতিটা Status বদল ঘটার সাথে সাথেই পায়।

BrowserEventSourceConnect হচ্ছে
Serverapi.example.com
GET /orders/42/stream · Accept: text/event-stream
ধাপ 1 / 6
1

new EventSource(url) একটা সাধারণ GET পাঠায়। Accept Header শুধু ইঙ্গিত; আসল কথা হলো Response-এর Type।

নিজেকে যাচাই করোপ্রশ্ন

একটা Server এই লাইনগুলো লেখে: “id: 7”, “data: hello”, “data: world”, তারপর একটা ফাঁকা লাইন। Browser কী পৌঁছে দেয়?

#সুবিধা আর সীমাবদ্ধতা

সুবিধা। SSE চলে HTTP/1.1, HTTP/2 বা HTTP/3-এ, তাই Protocol বদলাতে হয় না, সাধারণত Firewall-ও বদলাতে হয় না: একই Load Balancer, Auth, Logging আর CDN (যদি Streaming চলতে দেয়) কাজ করে যায়। বানানো সহজ, Page-এর পুরো জীবনে একটাই Request, আবার Connect আর Resume ভেতরেই আছে, আর প্রতি Event-এর বাড়তি খরচ এক বহর Header-এর বদলে কয়েক Byte।

সীমাবদ্ধতা।

  • শুধু এক দিকে। Client এই Stream দিয়ে কিছু পাঠাতে পারে না। কিছু পাঠাতে হলে যথারীতি আলাদা একটা HTTP Request (একটা POST) করতে হয়। বেশিরভাগ Feature-এর (Notification, Feed, Progress, AI-এর উত্তর) জন্য এতে সমস্যা নেই; Typing Indicator সহ Chat বা Multiplayer Game-এর জন্য WebSocket বেশি মানানসই (Lesson ২.৬)।
  • শুধু UTF-8 Text। Binary Data Encode করতে হয় (Base64 প্রায় ৩৩% বাড়িয়ে দেয়), অথবা আরও ভালো, একটা URL পাঠাও, Client সেটা আলাদা করে আনবে।
  • HTTP/1.1-এ এক Origin-এ ছয়টা Connection। Browser এক Origin-এ সর্বোচ্চ ছয়টা HTTP/1.1 Connection খোলে, আর সেগুলো সব Tab মিলে ভাগ করে। প্রতিটা SSE Stream Page খোলা থাকা পর্যন্ত একটা দখল করে রাখে, তাই সপ্তম Tab-এর Stream, আর সেই Tab থেকে একই Origin-এ যাওয়া বাকি সব Request, অনন্তকাল অপেক্ষা করে। HTTP/2 আর HTTP/3 একটা Connection-এ অনেক Stream চালিয়ে (Server সাধারণত একসাথে ১০০ বা তার বেশি চলতে দেয়) সমস্যাটা মিটিয়ে দেয়। Browser নির্মাতারা এই সীমাকে ইচ্ছাকৃত মনে করে, বাড়াবে না; তাই SSE দাও HTTP/2-তে, নয়তো একটা Tab (বা একটা SharedWorker) Stream ধরে রাখুক আর BroadcastChannel দিয়ে বাকিদের Event ভাগ করে দিক।

সপ্তম Tab: HTTP/1.1-এ এক Origin-এ ছয়টা Connection

Browser এক Origin-এ সর্বোচ্চ ছয়টা HTTP/1.1 Connection খোলে, আর সব Tab সেগুলো ভাগ করে নেয়। একটা SSE Stream Page খোলা থাকা পর্যন্ত একটা Connection দখল করে রাখে।

পরিস্থিতি

একজন ব্যবহারকারী একই Dashboard সাতটা Tab-এ খোলে।

Tab ১–৬প্রতিটায় একটা StreamPool: ৬-এর ৬টাই ব্যস্ত
Tab ৭
Serverapp.example.com
৬ × GET /stream (৬টা Connection)
ধাপ 1 / 3
1

প্রতিটা Tab নিজের Stream খোলে, আর প্রতিটা Stream একটা করে Connection অনির্দিষ্টকাল খোলা রাখে।

#Production-এ SSE

SSE-এর বেশিরভাগ Bug তোমার Code-এ থাকে না; থাকে তোমার Code আর Browser-এর মাঝখানের যন্ত্রগুলোতে, যেগুলো বানানো হয়েছিল শেষ হয়ে যাওয়া Response-এর কথা ভেবে।

#Proxy আর Compression-এর Buffering

Reverse Proxy সাধারণত Response Buffer করে: Nginx App-এর Output Memory Buffer-এ পড়ে নিয়ে বড় বড় টুকরোয় সামনে পাঠায় (Lesson ১.১৩)। ছোট ছোট Event-এর Stream-এর বেলায় এর মানে, Buffer না ভরা বা Stream শেষ না হওয়া পর্যন্ত Browser-এ কিছুই যায় না, তারপর সব একসাথে। চেনা লক্ষণ: Localhost-এ চলে, Production-এ দমকে দমকে আসে।

  • App থেকে X-Accel-Buffering: no পাঠাও (Nginx প্রতিটা Response ধরে এটা মানে), নয়তো Stream-এর location-এ proxy_buffering off দাও।
  • বাকি প্রতিটা ধাপ দেখো: কিছু CDN, API Gateway আর Serverless Platform শুধু সম্পূর্ণ Response ফেরত দেয়। ওপরে কিছু বানানোর আগে খুঁজে দেখো “Response Streaming” চলে কি না।
  • Compression-ও Buffer করে। gzip বা Brotli Middleware কিছু বের করার আগে পুরো একটা Block-এর অপেক্ষা করে। text/event-stream-কে Compression থেকে বাদ দাও, নয়তো প্রতিটা Event-এর পরে Compressor Flush করো। Event ছোট, তাই হারানোর কিছু নেই।
  • অফিসের Antivirus Proxy কখনো কখনো Scan করার জন্য পুরো Response জমিয়ে রাখে। ওদের Setting তুমি বদলাতে পারবে না; Library-গুলো Long Polling-কে বিকল্প হিসেবে রেখে দেয় এর একটা কারণ এটাই।
location /stream {
    proxy_pass http://app;
    proxy_http_version 1.1;
    proxy_set_header Connection '';   # Upstream Connection টিকিয়ে রাখো
    proxy_buffering off;              # নয়তো App পাঠাক X-Accel-Buffering: no
    proxy_cache off;
    gzip off;
    proxy_read_timeout 1h;            # Heartbeat এমনিতেই একে ব্যস্ত রাখে
}

#Idle Timeout আর Heartbeat

এক মিনিট কোনো Event না থাকলে Load Balancer, Proxy আর NAT-এর কাছে Stream-টাকে মরা মনে হয়, আর তারা বন্ধ করে দেয় (৬০ সেকেন্ড খুব চেনা ডিফল্ট)। Browser আবার Connect করে, তাই মোটামুটি চলে; কিন্তু প্রতিটা Client প্রতি মিনিটে আবার Connect করে, আর মাঝের সব কিছু Replay-এর ভরসায় থাকে। সমাধান Heartbeat: প্রতি ১৫–৩০ সেকেন্ডে একটা Comment লাইন (: ping) লেখো, পথের সব Idle Timeout-এর চেয়ে নিরাপদে কম। বাড়তি লাভ: Heartbeat লিখতে গিয়ে ব্যর্থ হলে Server বুঝে যায় Client নেই, তাই ভূতুড়ে Subscriber রেখে না দিয়ে Connection খালি করতে পারে।

Proxy পেরিয়ে Stream: Buffering আর Idle Timeout

Browser আর তোমার App-এর মাঝে প্রায় সবসময় একটা Reverse Proxy বা Load Balancer থাকে। সাধারণ Response-এর জন্য বানানো বলে সেটা Event আটকে রাখতে বা চুপচাপ Connection কেটে দিতে পারে।

পরিস্থিতি

SSE Endpoint-এর সামনে Nginx-এর ডিফল্ট Proxy Setting।

Browser
Proxy / LBNginx, ৬০ s Idle
App Server
GET /stream
ধাপ 1 / 6
1

Browser Proxy-র মধ্য দিয়ে Stream খোলে।

#Authentication: EventSource Header বসাতে পারে না

EventSource নেয় শুধু একটা URL আর একটা Option, withCredentials। সে Body পাঠাতে পারে না, GET ছাড়া অন্য Method পারে না, আর Authorization: Bearer …-এর মতো নিজের Header-ও পারে না। কাজের উপায়গুলো:

উপায়কীভাবেকী খেয়াল রাখবে
Cookieএকই Site-এর Session Cookie, বা অন্য Origin-এর জন্য withCredentials: true আর Credential সহ CORSState বদলায় এমন যেকোনো Endpoint-এ CSRF-এর নিয়ম তবুও খাটে (Lesson ১.৩)
URL-এ Token/stream?ticket=…URL জমা হয় Access Log, Proxy আর Browser History-তে। Authenticated একটা POST দিয়ে পাওয়া স্বল্পমেয়াদি, একবার ব্যবহারের Ticket দাও, দীর্ঘমেয়াদি Token কখনো না
Streaming Body সহ fetch()response.body (একটা ReadableStream) পড়ো, TextDecoder দিয়ে Decode করো, ফাঁকা লাইনে নিজে ভাগ করো, বা ছোট একটা Library নাওযেকোনো Method, Body আর Header চলে, কিন্তু Built-in Reconnect আর Last-Event-ID হারাও; নিজে বানাতে হয়

#Server-এর Resource, Scaling আর Deploy

নোট বলছে SSE-তে Backend-এর বাড়তি খরচ কম। প্রতি Event-এর হিসাবে কথাটা ঠিক: কয়েক Byte, কোনো Header নেই, Request Parse করা নেই। কিন্তু প্রতিটা যুক্ত Client মানে মিনিটের পর মিনিট, ঘণ্টার পর ঘণ্টা ধরে রাখা একটা খোলা Connection:

  • Event-loop Server ব্যবহার করো (Long Polling-এর মতোই)। Process-এর File Descriptor-এর সীমা বাড়াও; প্রতিটা Connection একটা Socket।
  • সংখ্যাটা মাথায় রেখে Plan করো: ১০ লাখ যুক্ত ব্যবহারকারী মানে ১০ লাখ খোলা Connection, যথেষ্ট Server আর Load Balancer-এর ক্ষমতায় ছড়ানো। ১০ লাখ Client-এ প্রতি ২০ সেকেন্ডে Heartbeat মানে সেকেন্ডে ৫০,০০০ ছোট Write: সস্তা, তবে বিনা পয়সায় না।
  • Fan-out: এক Server-এ তৈরি Event-কে বাকি সব Server-এ যুক্ত Client-দের কাছে পৌঁছাতে হবে। Event একটা Pub/Sub Channel-এ Publish করো, যেটায় প্রতিটা SSE Server Subscribe করে আছে (Lesson ২.৬ আর Chapter ৩০–৩২)।
  • Deploy: Restart মানে ওই Server-এর সব Stream কেটে যাওয়া। ধীরে ধীরে খালি করো (Drain): নতুন Stream নেওয়া বন্ধ করো, পুরনোগুলো এলোমেলো retry: দিয়ে দলে দলে বন্ধ করো, আর Event Log Process-এর বাইরে রাখো, যাতে যেকোনো Server Last-Event-ID থেকে Replay করতে পারে।

#LLM-এর Token Stream করতে SSE

ChatGPT, Claude বা Gemini-কে প্রশ্ন করলে উত্তর আসে শব্দে শব্দে। লম্বা একটা উত্তর তৈরি হতে ১০–৩০ সেকেন্ড লাগতে পারে, অথচ প্রথম শব্দগুলো দেখা যায় এক সেকেন্ডের মধ্যেই। প্রায় সব ক্ষেত্রেই সেই Stream হলো SSE: OpenAI আর Anthropic-সহ অনেকের API Request-এ "stream": true দিলে text/event-stream ফেরত দেয়।

WebSocket কেন না, SSE কেন?

  • Data চলে এক দিকে। Prompt ওপরে যায় একবার, সাধারণ একটা POST-এ; Token নেমে আসে নিচে। উত্তরের মাঝপথে ওপরে কিছু পাঠানোর দরকার নেই, শুধু “থামো” ছাড়া, আর Connection বন্ধ করাই সেটা বলে দেয়।
  • একটা Request, একটা উত্তর। এটা হুবহু HTTP-র সাথে মেলে: বাকি সব API Call-এর মতোই একই API Key, Auth, Rate Limit, Load Balancer, Logging আর Billing। WebSocket হলে নিজের Connection সামলানো আর Message Route করার আলাদা ব্যবস্থা লাগত।
  • প্রথম Token কত তাড়াতাড়ি এল, ব্যবহারকারী সেটাই টের পায়। Streaming ১৫ সেকেন্ডের অপেক্ষাকে বানিয়ে দেয় আধা সেকেন্ডের অপেক্ষা, তারপর পড়তে থাকা।

Stream দেখতে কেমন, সেটা Provider ভেদে আলাদা। Anthropic-এর Messages API পাঠায় ধরন দেওয়া Event (message_start, লেখার একটা টুকরো সহ content_block_delta, message_stop, মাঝে মাঝে ping); OpenAI-এর Chat Completions API পাঠায় data: {…} টুকরো, আর শেষ করে data: [DONE] দিয়ে। এই Request-গুলো Authorization Header সহ POST, তাই Client এগুলো পড়ে fetch Streaming দিয়ে, EventSource দিয়ে না।

সাধারণ একটা Product API Key রাখে নিজের Backend-এ; Backend Model-এর Stream পড়ে আর প্রতিটা টুকরো নিজের SSE Stream হিসেবে Browser-এ পাঠায়। সেখানে তিনটা জিনিস গড়বড় হয়:

  1. পথের যেকোনো জায়গায় Buffering (Proxy, Compression, একটা Gateway) Stream-কে একটা দেরিতে আসা পিণ্ড বানিয়ে দেয়: AI Feature-এ সবচেয়ে চেনা Bug।
  2. Cancel ওপর পর্যন্ত পৌঁছাতে হবে। ব্যবহারকারী Stop চাপলে Browser তার fetch Abort করে। তোমার Backend-কে Disconnect টের পেয়ে Upstream Request-ও বন্ধ করতে হবে, নইলে কেউ পড়বে না এমন Token-এর দাম দিতেই থাকবে।
  3. Model থেকে Resume নেই। মাঝপথে Connection কাটলে Model API কোনো Event ID থেকে আবার শুরু করতে পারে না। হয় পুরো Request আবার পাঠাও (খরচ আছে, উত্তরও বদলে যেতে পারে), নয়তো Backend একটা Response ID ধরে উত্তর তৈরি চালিয়ে যাক আর Store-এ লিখুক, যাতে Page Last-Event-ID দিয়ে তোমার Stream-এ আবার যুক্ত হতে পারে।

LLM-এর উত্তর Token ধরে ধরে Stream করা

Chat Assistant উত্তর তৈরি হতে হতেই দেখায়। Model API তোমার Backend-এ SSE Event Stream করে, আর তোমার Backend সেগুলো আবার Browser-এ Stream করে।

পরিস্থিতি

এমন একটা প্রশ্ন, যার পুরো উত্তর তৈরি হতে প্রায় ৮ সেকেন্ড লাগে।

BrowserChat Page
তোমার BackendAPI Key এখানেই থাকে
Model APIllm.example.com
POST /chat · {"prompt": …}
ধাপ 1 / 7
1

Body-তে Prompt দিয়ে fetch-এর মাধ্যমে একটা POST। EventSource এটা পারে না: সে শুধু GET পাঠায়, আর Authorization Header যোগ করতে পারে না।

#যখন Client কোনো Browser Tab না: Webhook আর Mobile Push

Polling আর SSE-র জন্য Client-কে চালু আর যুক্ত থাকতে হয়। যেখানে সেটা নেই, সেখানে কাজ করে আরও দুটো Push-এর পথ।

#Webhook: Server থেকে Server-এ Push

Webhook হলো একটা HTTP POST, যেটা এক কোম্পানির Server তোমার Register করা একটা URL-এ পাঠায়, তাদের দিকে কিছু ঘটলে: একটা Payment সফল হলো (Stripe, bKash বা অন্য Gateway), একটা Commit Push হলো (GitHub), একটা Message পৌঁছাল। এখানে তোমার “Client” হলো তোমার নিজের Server, যাকে বাইরে থেকে পৌঁছানো যায়, তাই Provider সোজা তাকে ডাকতে পারে। প্রতি মিনিটে Provider-এর API Poll করার বদলে তোমাকে জানিয়ে দেওয়া হয়।

Webhook হলো At-least-once: তুমি 2xx না দেওয়া পর্যন্ত Provider চেষ্টা করেই যায়, তাই তুমি ব্যর্থতা, দেরি আর Duplicate দেখবেই। একটা মজবুত Receiver:

  • Signature যাচাই করে। Provider প্রতিটা Request সই করে শুধু তোমার সাথে ভাগ করা একটা Secret দিয়ে, সাধারণত Timestamp আর Raw Body-র একটা HMAC-SHA256 (যেমন Stripe-এর Stripe-Signature: t=…,v1=… বা GitHub-এর X-Hub-Signature-256)। Raw Byte-এর ওপর আবার হিসাব করো (আবার Serialise করা JSON-এর ওপর না), Constant-time-এ তুলনা করো, আর কয়েক মিনিটের বেশি পুরনো Timestamp বাতিল করো (Stripe-এর Library-তে ডিফল্ট ৫ মিনিট), যাতে আগে ধরে রাখা একটা Request পরে আবার চালানো না যায়। তোমার URL সবার জন্য খোলা; এটা না থাকলে যে কেউ “Payment সফল” POST করে দিতে পারে।
  • Idempotent থাকে। প্রতিটা Event ID Unique Key সহ লিখে রাখে, আর আগে Process করা ID বাদ দেয়। Duplicate-এর জন্যও 200 দাও, নইলে Provider চেষ্টা চালিয়েই যাবে।
  • দ্রুত উত্তর দেয়, কাজ করে পরে। Event Queue-তে রাখো আর এক-দুই সেকেন্ডের মধ্যে 2xx দাও। Provider কয়েক সেকেন্ড পরেই Timeout ধরে (GitHub অপেক্ষা করে ১০ সেকেন্ড) আর ধীর উত্তরকে ব্যর্থতা ধরে, তাতেই তৈরি হয় সেই Duplicate, যেটা সামলাতে তুমি এইমাত্র শিখলে।
  • আসার ক্রমে ভরসা করে না। Retry-এর পরে “Refunded” আসতে পারে “Paid”-এর আগেই। Event-এর Timestamp দেখো, নয়তো Provider-এর API থেকে এখনকার অবস্থা আনো।
  • মিলিয়ে দেখে (Reconcile)। Retry-এর নিয়ম আলাদা আলাদা: Stripe Exponential Back-off সহ তিন দিন পর্যন্ত চেষ্টা করে; GitHub নিজে থেকে আবার পাঠায় না (হাতে Redeliver করতে হয়)। মাঝে মাঝে একটা Job চালাও, যেটা Provider-এর API থেকে সাম্প্রতিক Event-এর তালিকা এনে কখনো না পৌঁছানো কিছু থাকলে ধরে ফেলে।

Webhook: Retry, Signature আর Idempotency

একটা Payment Provider তোমার HTTPS Endpoint-এ Event Push করে। Delivery অন্তত একবার (At-least-once), তাই তোমার Endpoint-কে যাচাই করতে, Duplicate বাদ দিতে আর দ্রুত উত্তর দিতে হবে।

পরিস্থিতি

Payment evt_1001 সফল হয়।

ProviderPayment
আক্রমণকারী
তোমার Endpoint/webhooks/pay
Queue + DB
POST · Signature: t=…, v1=HMAC
ধাপ 1 / 4
1

Provider একটা JSON Event POST করে। Signature Header-এ থাকে একটা Timestamp, আর “timestamp.body”-র HMAC-SHA256, এমন একটা Secret দিয়ে বানানো যা শুধু তুমি আর Provider জানো।

#Mobile Push: APNs আর FCM

পকেটের Phone প্রতিটা App-এর জন্য একটা SSE Stream বা Long Poll খোলা রাখতে পারে না: Battery বাঁচাতে Operating System পেছনে চলা App থামিয়ে দেয়, তাদের Connection-ও বন্ধ করে। তার বদলে OS নিজের Platform-এর Push Service-এর সাথে একটাই Connection রাখে, আর সব App-এর Notification সেটা দিয়েই আসে:

  • iPhone, iPad আর Mac-এর জন্য APNs (Apple Push Notification service); Android-এর জন্য FCM (Firebase Cloud Messaging), যেটা iOS আর Web-এর জন্যও ব্যবহার করা যায়। Browser-এর আছে এর সমতুল্য Web Push, যেটা একটা Service Worker-কে জাগায়।
  • App Register করে একটা Device Token পায়, আর সেটা তোমার Server-এ পাঠায়। জানানোর দরকার হলে তোমার Server সেই Token আর ছোট একটা Payload (বড়জোর প্রায় ৪ KB) দিয়ে APNs বা FCM-কে ডাকে।
  • Delivery শুধু চেষ্টা মাত্র (Best Effort): একটা Push দেরিতে আসতে পারে, Phone Offline থাকলে একই Collapse Key-এর সর্বশেষটায় মিশে যেতে পারে, বা হারিয়েও যেতে পারে। App Uninstall হলে Token বাতিল হয়ে যায় (APNs উত্তর দেয় 410 Unregistered); সেটা মুছে দাও।
  • তাই Push-কে ধরো ঘুম ভাঙানোর ডাক হিসেবে, Data হিসেবে না: App জেগে তার Cursor দিয়ে তোমার API থেকে Sync করে, ঠিক যেমন আবার Connect হওয়ার পরের একটা Long Poll।

Mobile Push: Apple আর Google-এর Push Service দিয়ে

পকেটের Phone প্রতিটা App-এর জন্য আলাদা Stream খোলা রাখতে পারে না। Operating System তার Platform Push Service-এর সাথে একটাই Connection রাখে, আর তোমার Server সেই Service-এর মধ্য দিয়ে যায়।

পরিস্থিতি

যার App বন্ধ, এমন ব্যবহারকারীর জন্য নতুন Chat Message।

তোমার Server
APNs / FCM
Phoneতোমার App ঘুমিয়ে
POST /devices · token
ধাপ 1 / 4
1

Install-এর সময় App OS-এর কাছ থেকে একটা Device Token নেয়, আর সেটা তোমার Server-এ পাঠায়; Server সেটা ব্যবহারকারীর নামে রেখে দেয়।

#Reconnect-এর ঝড়

Long Poll আর SSE Stream দীর্ঘস্থায়ী, তাই বিপদের মুহূর্ত সাধারণ Traffic না, বরং সবাই একসাথে আবার Connect করা: Deploy-এর পরে, Server Crash হলে, Load Balancer Failover হলে, বা Network একটু হোঁচট খেলে। চুপচাপ একটা Connection-এর চেয়ে Reconnect অনেক দামি: TCP আর TLS Handshake, Auth যাচাই, Last-Event-ID থেকে Replay-এর Query। ৫০,০০০ Client যদি স্থির একটা Retry Delay ধরে একই সেকেন্ডে এটা করে, বাকি Server-গুলো চাপে ডুবে যায়, Health Check-এ ফেল করে, আর নিজেদের Client-দেরও ফেলে দেয়, যারা আবার একসাথে চেষ্টা করে। Outage নিজেই নিজেকে খাওয়ায়।

বাঁচার উপায় সব জায়গায় একই (সাধারণভাবে Retry নিয়ে আবার আসবে Chapter ২৮-এ):

  • Client-এ Full Jitter সহ Exponential Back-off: প্রতিটা ব্যর্থতার পরে সীমা দ্বিগুণ করো (১, ২, ৪, ৮… সেকেন্ড, একটা সর্বোচ্চ পর্যন্ত), আর ০ থেকে সেই সীমার মধ্যে এলোমেলো একটা সময় অপেক্ষা করো। ৫০,০০০ Reconnect ৩০ সেকেন্ডে ছড়ালে সেকেন্ডে প্রায় ১,৭০০।
  • Server-ও সাহায্য করে: এলোমেলো মানের retry: পাঠায়, Deploy-এর আগে ধীরে ধীরে Connection খালি করে, আর Load Balancer Restart হওয়া Server-এ ধীরে ধীরে Traffic বাড়ায়।
  • খোলাখুলি Load ফেলে দাও: চাপে থাকা Server-এর উচিত দ্রুত না বলা (Retry-After সহ একটা 503), সামলাতে পারবে না এমন Connection নেওয়ার চেয়ে। মনে রেখো, 503 পেলে EventSource থেমে যায়, তাই তোমার onerror-কে Back-off সহ সেটা আবার চালু করতে হবে।
  • Long Polling-এর নিজস্ব ভিড়: একটা Broadcast Event ধরে রাখা সব Poll-এর উত্তর একসাথে দেয়, আর সেই সব Client একই মুহূর্তে আবার Poll করে। Broadcast-এর পরে আবার Poll করার আগে ছোট একটা এলোমেলো বিরতি দিলেই মেটে; SSE-তে এই সমস্যা নেই, কারণ সেখানে Connection খোলাই থাকে।

Restart-এর পরে Reconnect-এর ঝড়

১ লাখ Client দুটো Server-এর সাথে SSE বা Long-poll Connection ধরে আছে। একটা Server Restart হয়। এরপর Client-রা কী করে, তার ওপরই নির্ভর করে অন্যটা টিকবে কি না।

পরিস্থিতি

Connection কাটার ঠিক ১ সেকেন্ড পরে প্রতিটা Client আবার চেষ্টা করে।

Browser60,000
Phone40,000
Load Balancer
Server ১50,000
Server ২50,000
ধাপ 1 / 5
1

স্বাভাবিক অবস্থা: প্রতিটা Server-এ ৫০,০০০ দীর্ঘস্থায়ী Connection। চুপচাপ একটা Connection-এর খরচ সামান্য।

#সংখ্যা দিয়ে বেছে নেওয়া

Short PollingLong PollingServer-Sent Events
কে শুরু করেClient, Timer ধরেClient, প্রতিটা উত্তরের পরে আবারClient একবার; তারপর থেকে Server লেখে
খবরের দেরিগড়ে Interval-এর অর্ধেক, সর্বোচ্চ পুরো একটা Intervalমোটামুটি এক Round Trip (Cursor না থাকলে ফাঁকের ক্ষেত্রে ছাড়া)মোটামুটি এক একমুখী যাত্রা
RequestClient ÷ Interval, বেশিরভাগই খালিপ্রতি Event বা Timeout-এ মোটামুটি একটাপ্রতি Connection-এ একটা (সাথে Reconnect)
Server-এ ধরে রাখাদুই Poll-এর মাঝে কিছু নাপ্রতি Client-এ একটা খোলা Requestপ্রতি Client-এ একটা খোলা Connection
হারানো Eventঅবস্থা পড়লে কিছু হারায় না; Event-এর জন্য Cursor লাগেCursor আর Log লাগেid + Last-Event-ID + Log, ভেতরেই আছে
Infrastructureযেকোনো কিছু, CDN Cache সহসব Idle সীমার চেয়ে কম TimeoutBuffering নেই, Heartbeat, অনেক Tab-এর জন্য HTTP/2
দিকClient টানেClient টানেশুধু Server → Client
কোথায় মানায়কালেভদ্রে Update, ছোট বহর, CDN-এর পেছনে সবার একই Dataবৈরী Network-এ বিকল্প; Program Client (Queue, Bot)Notification, Feed, Live Dashboard, Progress Bar, AI-এর Token Stream

একই বহর সংখ্যায়: ১০ লাখ যুক্ত Client, প্রতি Client-এ ১০ মিনিটে একটা খবর:

Short Polling (৫ s)Long Polling (৩০ s ধরে রাখা)SSE (২০ s Heartbeat)
প্রতি সেকেন্ডে Request২,০০,০০০প্রায় ৩৪,০০০প্রায় ০ (শুধু Reconnect)
খালি উত্তরপ্রায় ৯৯%প্রায় ৯৫% (Timeout)নেই
গড় দেরি২.৫ sমোটামুটি এক Round Tripমোটামুটি এক একমুখী যাত্রা
খোলা Connectionঅল্প১০,০০,০০০১০,০০,০০০
বাড়তি Trafficপ্রায় ১.৩ Gbps Headerপ্রায় ০.২ Gbpsপ্রায় ৫ Mbps (Event আর Heartbeat)

“Real-time”-এর দাম কত? Short Polling বনাম Long Polling বনাম SSE

একটা Preset বেছে নাও, নয়তো Input নিজে ঠিক করো। Long Poll ৩০ সেকেন্ড ধরে রাখা হয়; SSE প্রতি ২০ সেকেন্ডে একটা Heartbeat পাঠায়। সংখ্যাগুলো পুরো বহরের, প্রতি সেকেন্ডের হিসাবে।

Preset

যুক্ত Client
Short Poll-এর Interval
প্রতি Client-এ ঘণ্টায় খবর

Short Polling

প্রতি সেকেন্ডে Request
200k
খালি উত্তর
99.2%
খবরের গড় দেরি
2.5 s
খোলা রাখা Connection
দুই Poll-এর মাঝে নেই
বাড়তি Traffic
1.3 Gbps

Long Polling

প্রতি সেকেন্ডে Request
34k
খালি উত্তর
95.1%
খবরের গড় দেরি
≈ এক Round Trip
খোলা রাখা Connection
1.0M
বাড়তি Traffic
218.0 Mbps

SSE

প্রতি সেকেন্ডে Request
শুধু Reconnect
প্রতি সেকেন্ডে Event + Heartbeat লেখা
52k
খবরের গড় দেরি
≈ একমুখী যাত্রা
খোলা রাখা Connection
1.0M
বাড়তি Traffic
5.2 Mbps
i

১০ লাখ Order Page প্রতি ৫ সেকেন্ডে Poll করছে, প্রতিটা Order বদলায় মোটামুটি ১০ মিনিটে একবার। বড় বহর: Short Polling-এর বেশিরভাগ Request খরচ হয় “কিছু বদলায়নি” বলতে। Long Polling Request কমিয়ে আনে মোটামুটি প্রতি Event বা Timeout-এ একটায়; SSE-তে Request লাগেই না, শুধু প্রতি Client-এ একটা খোলা Connection আর কয়েকটা ছোট Write। এখানেই SSE (বা বিকল্প হিসেবে Long Polling) লাভজনক।

কাজের একটা সিদ্ধান্তের পথ:

  1. Client-কেও কি ঘন ঘন পাঠাতে হয় (Typing সহ Chat, Game, একসাথে Edit)? তাহলে WebSocket (Lesson ২.৬)।
  2. Receiver কি আরেকটা Server? তাহলে Webhook। App বন্ধ থাকা একটা Phone? Mobile Push, তারপর Sync।
  3. কয়েক সেকেন্ড দেরি কি চলে, বা Data কি সবার জন্য একই? তাহলে Short Polling, যতটা পারো Conditional Request আর CDN সহ।
  4. নইলে (একমুখী Update, যেটা তাৎক্ষণিক মনে হওয়া চাই): SSE, HTTP/2-তে, ID, Replay Log আর Heartbeat সহ। তোমার ব্যবহারকারীরা যদি Streaming ভেঙে দেওয়া Proxy-র পেছনে থাকে, Long Polling বিকল্প হিসেবে রাখো।

#System Design-এ Real-time Push

Interviewer আর Production Incident যা জানতে চায়:

  • আগে Latency-র বাজেট ঠিক করো। “১ সেকেন্ডের মধ্যে” আর “৩০ সেকেন্ডের মধ্যে” দুটো আলাদা Design-এ নিয়ে যায়। “Real-time” লেখা অনেক Requirement-এর আসল মানে “কয়েক সেকেন্ড”।
  • জানানো আর Data আলাদা রাখো। Push (একটা Poll-এর ফল, একটা SSE Event, একটা Mobile Push, একটা Webhook) বলে কিছু বদলেছে; তারপর Client Cursor দিয়ে আসল উৎস থেকে পড়ে। তাতে Duplicate, ফাঁক আর Reconnect নিরীহ হয়ে যায়।
  • প্রতিটা Event-এর ক্রমমতো একটা ID আর ছোট একটা Replay Log লাগে। এগুলো ছাড়া বিচ্ছিন্নতার পরে Resume, Duplicate বাদ দেওয়া বা Event সাজানো, কোনোটাই সম্ভব না।
  • Stateful Connection Scaling বদলে দেয়। Long Poll আর SSE Stream একটা Client-কে মিনিটের পর মিনিট একটা Server-এ বেঁধে রাখে। লাগে Server-গুলোর মধ্যে Pub/Sub Fan-out, Connection বুঝে Load Balancing, File Descriptor আর Memory-র বাজেট, আর Deploy-এর জন্য ধীরে খালি করা।
  • খরচ গোনো: Polling-এ প্রতি সেকেন্ডে Request, Long Polling আর SSE-তে খোলা Connection, আর ব্যর্থতার পরে প্রতি সেকেন্ডে Reconnect।
  • ঝড়ের জন্য তৈরি থাকো: Jitter সহ Back-off, এলোমেলো retry:, ধীরে খালি করা, Load ফেলে দেওয়া।
  • নিজের যন্ত্রগুলো চেনো: Application Code-এর চেয়ে অনেক বেশি বার SSE ভাঙে Proxy Buffering, Compression, Idle Timeout আর ছয় Connection-এর সীমায়।
লক্ষণসম্ভাব্য কারণসমাধান
Event দমকে দমকে আসে, বা শুধু Stream শেষ হলে আসেProxy Buffering বা CompressionX-Accel-Buffering: no, proxy_buffering off, text/event-stream-এ gzip না
প্রতি ৬০ সেকেন্ডে Stream কেটে যায়Proxy, Load Balancer বা NAT-এর Idle Timeoutপ্রতি ১৫–৩০ সেকেন্ডে Heartbeat Comment
সপ্তম Tab আটকে থাকে, অন্য Request-ওHTTP/1.1-এর ছয় Connection-এর সীমাHTTP/2, বা প্রতি Browser-এ একটা ভাগ করা Stream
Deploy-এর পরে কিছু Page আর কখনো Update হয় নাEventSource 502/503 পেয়ে চিরতরে বন্ধonerror-এ Back-off সহ আবার চালু করো; Deploy-এর আগে খালি করো
আবার Connect হওয়ার পরে ব্যবহারকারী Event মিস করেid: লাইন নেই, বা Replay Log নেইID, Log, Last-Event-ID (Long Polling-এ Cursor)
Long Poll 504 ফেরত দেয়ধরে রাখার সময় কোনো Proxy-র Timeout-এর চেয়ে বেশি২৫–৩০ সেকেন্ড ধরে রাখো
Restart-এর পরে CPU লাফিয়ে ওঠে, একের পর এক পতনReconnect-এর ঝড়Full Jitter সহ Exponential Back-off, ধীরে খালি করা
গ্রাহক দুটো রসিদ পায়Webhook Retry হয়েছে, Handler Idempotent নাEvent ID দিয়ে Duplicate বাদ, দ্রুত উত্তর, Queue থেকে কাজ

#নিজে করে দেখো

# ১. সত্যিকারের একটা Public SSE Stream দেখো: Wikimedia-র Edit-এর Live Feed। -N দিলে curl Buffer করে না।
curl -N -A 'sse-lesson/1.0 (you@example.com)' \
  https://stream.wikimedia.org/v2/stream/recentchange | head -n 20
#   event: message
#   id: [{"topic":"…","partition":0,"timestamp":…}]
#   data: {"$schema":"/mediawiki/recentchange/1.0.0","meta":{…},"type":"edit",…}

# ২. এই Lesson-এর Node.js Server চালাও (node server.mjs), তারপর:
curl -N http://localhost:8080/stream                        # প্রতি ৩ s-এ Live Event, প্রতি ২০ s-এ Heartbeat
curl -N -H 'Last-Event-ID: 5' http://localhost:8080/stream  # আগে id 5-এর পরের সব Replay করে

# ৩. Conditional Short Polling: কিছু না বদলালে দ্বিতীয় Request-এ 304 আসার কথা
curl -si https://example.com/ | grep -i etag
curl -si -H 'If-None-Match: "<ওপরের etag>"' https://example.com/ | head -n 1

# ৪. যে Long Poll ধরে রাখা হচ্ছে, সেটা চোখে দেখো: SQS একটা Message-এর জন্য ২০ s পর্যন্ত অপেক্ষা করে
aws sqs receive-message --wait-time-seconds 20 \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/orders

Browser-এ যেকোনো Page-এ Developer Tools-এর Console খুলে চালাও:

const es = new EventSource('https://stream.wikimedia.org/v2/stream/recentchange');
es.onmessage = (e) => console.log(JSON.parse(e.data).title);
// পরে: es.close()

তারপর Network Panel খোলো, recentchange Request-এ Click করে তার EventStream Tab দেখো। Panel-এর Throttling Menu থেকে কয়েক সেকেন্ডের জন্য Network বন্ধ করে আবার চালু করো: দেখবে Request ব্যর্থ হলো, নিজে থেকেই নতুন একটা শুরু হলো, আর তার Request Header-এ আছে Last-Event-ID।

#সারসংক্ষেপ

মূল শিক্ষা

  • HTTP শুরু করে Client, আর NAT-এর পেছনের Browser-এর কাছে পৌঁছানো যায় না; তাই প্রতিটা Push কৌশল ব্যবস্থা করে যাতে খবর আসার মুহূর্তে Client-এর একটা Request বা Connection অপেক্ষায় থাকে। মূল পাঁচটা হলো Short Polling, Long Polling, SSE, WebSocket আর WebRTC; Tab-এর বাইরে আছে Webhook আর Mobile Push।
  • Short Polling প্রতি Interval-এ জিজ্ঞেস করে: দেরি গড়ে Interval ÷ ২, সর্বোচ্চ পুরো একটা Interval; খরচ প্রতি সেকেন্ডে Client ÷ Interval Request, বেশিরভাগই খালি (১০ লাখ Client প্রতি ৫ সেকেন্ডে = সেকেন্ডে ২ লাখ Request, প্রায় ৯৯% খালি)। সস্তা করো If-None-Match/304, সবার একই Data-র CDN Cache, অবস্থা বুঝে Interval আর Jitter দিয়ে। setInterval-এর বদলে setTimeout পরপর জোড়ো।
  • Long Polling (Hanging GET) খবর বা Timeout পর্যন্ত Request ধরে রাখে, তারপর Client একই Keep-alive Connection-এ সাথে সাথে আবার চায়। ধরে রাখো ২৫–৩০ সেকেন্ড, সব Proxy আর Load Balancer-এর Timeout-এর (প্রায়ই ৬০ সেকেন্ড) চেয়ে কম। একটা Cursor (after=<শেষ ID>) আর Server-এর Log রাখো, যাতে ফাঁকের Event না হারায়; একবারে একটাই Poll; Jitter সহ Back-off-এ Retry; Event-loop Server।
  • SSE হলো কখনো শেষ না হওয়া একটা text/event-stream Response, যাতে থাকে data, event, id, retry আর Comment লাইন। EventSource নিজে নিজে আবার Connect করে আর Last-Event-ID পাঠায়; Log রাখতে হয় Server-কে। এটা একমুখী, শুধু UTF-8, আর HTTP/1.1-এ এক Origin-এ ছয় Connection-এ আটকা (HTTP/2 নাও)। 200 ছাড়া অন্য উত্তর পেলে EventSource চিরতরে থেমে যায়।
  • Production-এ SSE ভাঙে Proxy Buffering (X-Accel-Buffering: no), Compression, Idle Timeout (প্রতি ১৫–৩০ সেকেন্ডে Heartbeat) আর Auth-এ (EventSource Header বসাতে পারে না: Cookie, স্বল্পমেয়াদি Ticket, বা fetch Streaming)।
  • LLM API Token Stream করে SSE দিয়ে, কারণ Data একমুখী, একটা Request মানে একটা উত্তর, আর ব্যবহারকারী টের পায় প্রথম Token কত তাড়াতাড়ি এল। Cancel ওপর পর্যন্ত পৌঁছে দাও; Model থেকে Resume নেই।
  • Webhook হলো At-least-once Server থেকে Server-এ POST: Raw Body-র ওপর Timestamp সহ HMAC Signature যাচাই করো, Event ID দিয়ে Duplicate বাদ দাও, দ্রুত উত্তর দিয়ে কাজ Queue-তে রাখো। APNs/FCM OS-এর একটাই Connection দিয়ে Best-effort Mobile Push পৌঁছায়: Push-কে ঘুম ভাঙানোর ডাক ধরো, তারপর Cursor দিয়ে Sync।
  • Restart-এর পরে Reconnect-এর ঝড় ঠেকাও Full Jitter সহ Exponential Back-off, এলোমেলো retry: আর ধীরে খালি করা দিয়ে।