C.W.K.
Stream
Lesson 01 of 05 · published

Polling → Long-Polling — WebSocket 전 싼 real-time

~10 min · streaming-async, polling, long-polling

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"SSE 와 WebSocket 전 server-push update 받는 유일 방법이 반복 묻기. Naive polling 이 낭비; long-polling 이 낭비성 fix; 둘 다 더 고기술 옵션 안 맞는 경우 위해 2026 production 에 여전히 ship."

Naive Polling — 반복 묻기, 대부분 답 버리기

Client 가 N초 마다 server 한테 "update 있어?" 물음. 변경 없으면 server 가 빈 결과 돌려줌; client 가 기다리고 또 물음. 구현 단순; 어느 HTTP server 와 동작; 세상 모든 proxy 생존.

비용:

  • 지연. 평균 전달 시간 = N/2. 5s 마다 polling 면 user-인지 지연 0과 5s 사이, 평균 2.5s.
  • Bandwidth. 빈 response 도 HTTP roundtrip 비용 (request line + header + 200 OK + 빈 body = 양쪽 ~500 byte). 10,000 client 간 5s interval 이면 1 MB/s 의 아무것도 아님.
  • Server 부하. Idle client 도 endpoint 끊임없이 침.

괜찮은 때: 저-stake 상태 체크 (10s 마다 build 진행), background worker, 지연 tolerance 높은 곳.

Long-Polling — Server 가 request 보유

같은 패턴, 더 똑똑한 실행: client 가 물음; 가용 없으면 server 가 즉시 응답 안 함. 대신 server 가 event 기다리며 연결 열어둠. 뭔가 일어나면 (혹은 server 쪽 timeout 발동, ~30s), server 가 event (혹은 빈 + 'timeout' marker) 로 응답. Client 가 즉시 새 long-poll 시작.

Naive polling 대비 승리:

  • 거의 zero 지연. 데이터 가용 순간 server push. Client 가 한 RTT 안에 받음.
  • 낮은 request rate. Event 당 request 하나 (혹은 ~30s timeout 당) 5s 당 request 하나 대신.

비용:

  • Pending client 당 영속 연결 하나. Server 가 N 대기 client 위해 N file descriptor 보유. Async/await framework (FastAPI, Node, Go) 가 이거 싸게 처리; thread-per-request server (옛 PHP, Apache prefork) 가 안 함.
  • Proxy/timeout 간섭. 일부 intermediary 가 ~60s 침묵 후 연결 닫음. Server 쪽 timeout 이 어느 intermediary 보다 먼저여야.

Async 비용 반전

동기 (thread-per-request) server 에서 10,000 long-poll 연결 보유가 10,000 thread 필요 — 보통 비현실적. Async server (Python asyncio, Node.js, Go goroutine) 에서 10,000 pending 연결이 거의 비용 영 — 각각 stalled coroutine, thread 아님. Framework concurrency 모델이 scale 에서 long-polling 실용성 결정.

Long-polling 이 HTTP 동작하는 어디서나 동작하는 가장 단순한 'real-time' 패턴. Protocol upgrade 불필요, 특별 middleware 불필요, client-쪽 라이브러리 불필요. SSE 나 WebSocket 못 쓸 때 (회사 방화벽 차단, 고대 client) long-polling 이 그냥 동작하는 fallback.

Long-polling 이 SSE / WebSocket 이기는 때

Long-polling 이 SSE 이김:

  • Client 가 장기 연결 떨어뜨리는 방화벽 뒤 (일부 회사 환경) — 근데 SSE 도 같은 문제.
  • 각 event 에 request/response semantics 필요 (client 가 각 전달 ack 원함). SSE 가 one-way push 만.

Long-polling 이 SSE/WebSocket 한테 짐:

  • 높은 event 빈도 기대. 각 event 가 long-poll 재오픈 — SSE 안 가진 per-event HTTP overhead.
  • 진짜 스트리밍 원함 (server 가 작은 event 의 연속 흐름 push). SSE 가 이를 위해 설계.
  • 양 방향 트래픽 필요. 양 방향이 real-time 흘러야 → WebSocket.

cwkPippa 의 streaming 스택

cwkPippa 가 polling 도 long-polling 도 안 씀 — AI token stream 위해 SSE ship (canonical 사용 사례: WebUI 한테 server-push 높은 빈도 event). 스트리밍 안 하는 endpoint 의 fallback 이 일반 request/response. Cinder Sidekick 채널 (진짜 양방향) 위해 WebSocket 존재. Long-polling 절대 필요 없었음 — ship 한 모든 client 가 SSE native 지원. 패턴이 2026 에 여전히 ship — Pushpin, Pusher, 많은 SaaS 알림 service 가 SSE/WebSocket 실패 시 long-polling 을 fallback 으로 씀.

Code

Naive polling — N초 마다 묻고, 대부분 응답 버림·python
# Naive polling — client 쪽
import httpx, time

while True:
    resp = httpx.get('https://api.example.com/inbox/poll?since_id=last_seen')
    events = resp.json()['events']
    for event in events:
        handle(event)
    time.sleep(5)  # 5초 마다 poll

# 낭비: 대부분 poll 이 빈 응답.
Long-polling server (FastAPI asyncio) — 데이터나 timeout 까지 request 보유·python
# Long-polling — server 쪽 (FastAPI + asyncio)
import asyncio
from fastapi import FastAPI, Request

app = FastAPI()
_event_queues: dict[str, asyncio.Queue] = {}

@app.get('/inbox/poll')
async def long_poll(client_id: str, request: Request):
    queue = _event_queues.setdefault(client_id, asyncio.Queue())
    # 최대 30s 동안 event 기다림 (server-쪽 timeout)
    try:
        event = await asyncio.wait_for(queue.get(), timeout=30.0)
        return {'event': event}
    except asyncio.TimeoutError:
        # 30s 내 event 없음 — client 가 즉시 재 poll 하도록 빈 거 돌려줌
        return {'event': None}

# 뭔가 일어나면 queue 에 push (어느 consumer 든):
async def publish_to(client_id: str, event: dict):
    queue = _event_queues.get(client_id)
    if queue:
        await queue.put(event)

# Client 쪽 — naive polling 과 같은 shape, sleep 불필요:
# while True:
#     resp = httpx.get('https://api.example.com/inbox/poll', params={'client_id': 'me'})
#     event = resp.json()['event']
#     if event: handle(event)
#     # 즉시 loop — server 가 다음 request 보유
Long-polling client — tight loop, sleep 불필요·javascript
// Long-polling — client 쪽 (브라우저 fetch loop)
async function longPoll(clientId) {
  while (true) {
    try {
      const resp = await fetch(`/inbox/poll?client_id=${clientId}`);
      const data = await resp.json();
      if (data.event) {
        handle(data.event);
      }
      // sleep 없음 — 즉시 재-poll. Server 가 다음 event 나 timeout 까지 보유.
    } catch (e) {
      // 네트워크 에러 — flapping server 두드림 피하려 잠시 back off
      await new Promise(r => setTimeout(r, 1000));
    }
  }
}

longPoll('me');

External links

Exercise

새 event 도착이나 30초 경과까지 GET /events?since_id=X 보유하는 FastAPI long-poll endpoint 만들어. client_id 당 asyncio.Queue 써. 그 다음 작은 publisher (매칭 queue 에 event 넣는 POST /publish) 쓰고 long-polling client 가 publish 의 ~10ms 안에 event 받는 거 검증 — 5s interval naive polling 대비, 최대 5s 기다림. timestamp 로 둘 다 수치 측정.
Hint
Asyncio 패턴: client_id → asyncio.Queue 매핑 dict, asyncio.wait_for timeout 으로 await queue.get(). Publishing 은 queue.put_nowait(event). Client: response 에 즉시 재-fetch 가진 fetch + tight loop. 수치 비교: t=0 에 publish, client 가 t=~RTT (long-poll) vs t=~2.5s 평균 (naive 5s polling) 에 받음. 차이가 느낄 만큼 극적.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.