블로그 목록
Backend18분 읽기

TTFB 분해 — DNS / TCP / TLS / 서버 처리 병목 찾기

TTFB를 DNS 조회, connection, TLS handshake, request 전송, server 처리, first-byte 전송 구간으로 나눕니다. curl timing 값과 browser Network panel을 함께 읽고 connection reuse·HTTP version·cache 상태를 기록해 병목을 구분합니다. ICMP ping만으로 HTTP 지연 원인을 판단할 수 없는 이유도 설명합니다.

TTFBDNSTCPTLSHTTP/3QUIC성능curl디버깅
목차(34개 항목)

가상의 서비스에서 "TTFB가 350ms이고 ping은 7ms로 측정됐습니다. 서버가 느린 건가요?"라는 질문을 받았다고 가정해 봅니다. 이 수치는 진단 절차를 설명하기 위한 예시이며 실제 서비스의 측정 결과가 아닙니다. 답을 하려면 먼저 TTFB가 무엇을 측정한 값인지부터 정리해야 합니다. TTFB는 서버 처리 시간만을 뜻하지 않습니다. 서버 코드가 실행되기 전후의 여러 구간이 함께 포함됩니다.

이 글은 요청 → 첫 바이트 도착까지의 모든 단계를 분해하고, 각 단계가 무엇을 하는지·어떻게 측정하는지·어디서 줄일 수 있는지 정리합니다.


0. 핵심 명제 — TTFB는 합산 측정값

TTFB는 측정 도구가 정한 시작점부터 첫 응답 바이트까지의 경과 시간입니다.

잘못된 이해정확한 이해
서버가 응답을 만드는 데 걸린 시간클라이언트가 측정한 합산 경과 시간

TTFB 안에 들어있는 것:

[브라우저]                                                     [서버]
  │                                                              │
  │── 1) DNS 조회       (도메인 → IP)            ────────►       │
  │── 2) TCP 핸드셰이크 (연결 통로 만들기)        ◄───SYN-ACK──── │
  │── 3) TLS 핸드셰이크 (암호화 협상)             ◄──ServerHello──│
  │── 4) HTTP 요청 전송                           ────GET─────►  │
  │                                                              │ ← 여기서 처음으로
  │                                                              │   서버 코드 실행
  │── 5) 첫 바이트 도착 (응답 헤더 시작)         ◄──HTTP/1.1 200─│
  └────────────────────────── TTFB ──────────────────────────────┘

핵심: curl의 time_starttransfer는 전송 시작부터 첫 바이트까지의 누적값이라 이름 해석·연결·TLS·프로토콜 협상과 서버 처리 시간을 포함합니다. 브라우저 Navigation Timing의 responseStart - requestStart는 요청 시작 이후만 계산하므로 DNS·연결 시간을 포함하지 않습니다. 먼저 어떤 지표를 말하는지 고정해야 합니다.


1. DNS 조회 — 여러 캐시와 재귀 resolver

브라우저는 service.example 같은 도메인 이름만으로 서버에 연결할 수 없습니다. DNS로 접속할 IP 주소를 확인하는 과정이 먼저 필요합니다. 아래 도메인과 IP는 흐름을 설명하기 위한 문서용 예시입니다.

이 변환에는 브라우저·OS·로컬 네트워크·재귀 resolver의 캐시가 관여할 수 있습니다. 구현과 네트워크 정책에 따라 일부 계층은 없거나 순서가 다릅니다.

흔히 관여하는 계층

위치설명
브라우저자체 호스트 캐시와 연결 정보를 재사용할 수 있음
OS resolver운영체제와 로컬 resolver 정책에 따라 캐시
공유기·사내 DNSDNS proxy/cache로 동작하는 구성도 있음
재귀 resolverISP 또는 사용자가 선택한 resolver가 권한 서버 질의를 대신 수행
권한 DNSroot·TLD 위임을 따라 도메인의 권한 서버가 최종 레코드를 제공

같은 사이트 두 번째 방문이 빠른 이유

첫 방문:    [클라이언트 캐시 miss] → [재귀 resolver] → [권한 DNS] → "76.76.21.21"

두 번째 방문: [브라우저] → "캐시에 있음" → 즉시
            (네트워크 질의 생략)

TTL(Time To Live) 이 만료되면 캐시가 새 질의를 해야 합니다. TTL은 레코드 운영자가 설정하므로 고정 범위로 일반화할 수 없습니다.

⑤ 권한 DNS 추적 — 트리 구조

                       . (root)
                  ┌──────┼──────┐
                .com   .app   .kr
                       │
              ┌────────┼────────┐
          service.example  docs.example
              │
        Authoritative NS (Vercel/Cloudflare 운영)
              │
          76.76.21.21  ← 진짜 IP

ISP DNS가 캐시에 답이 없으면 재귀 질의를 합니다. 위에서부터 차례로:

  1. ISP DNS → root: ".app은 누가 관리해?" → root: "이 IP에 .app TLD 서버"
  2. ISP DNS → 권한 서버 위임 정보 확인: "service.example은 어느 서버가 담당하나?"
  3. ISP DNS → 권한 NS: "접속할 주소를 알려줘" → 문서용 IP 응답

실제 지연은 resolver 캐시, 권한 서버 위치, 재시도와 네트워크 경로에 따라 측정해야 합니다.

시크릿 모드의 캐시?

시크릿 모드는 일반 프로필과 저장 상태를 분리하지만 OS·네트워크 계층까지 완전히 초기화하는 DNS 벤치마크 모드는 아닙니다. 브라우저 버전과 Secure DNS 설정도 함께 기록하세요.

진짜 비우려면:

# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder

# Windows
ipconfig /flushdns

# Chrome 네트워크 로그 수집
# chrome://net-export/

DoH(DNS over HTTPS)가 바꾸는 것

브라우저의 Secure DNS(DoH)는 현재 DNS 제공자를 보안 방식으로 업그레이드하거나 사용자가 선택한 제공자를 사용할 수 있습니다. 활성화되면:

  • OS DNS resolver를 우회 → ② OS 캐시가 안 잡힐 수 있음
  • 회사 보안장비/공유기가 DNS를 못 봄
  • 측정 도구로 OS 캐시 상태를 봐도 브라우저 실제 동작과 다를 수 있음

시사점: OS 캐시만 비운 결과로 브라우저 DNS 경로 전체가 초기화됐다고 단정하지 않습니다.


2. TCP 핸드셰이크 — 연결 통로 만들기

DNS로 IP를 알아냈으면 이제 그 IP와 신뢰할 수 있는 연결을 만들어야 합니다. TCP는 패킷이 잘 도착했는지, 순서대로 도착했는지 보장하는 프로토콜.

3-way handshake

[브라우저]                            [서버]
    │                                   │
    │── SYN ───────────────────────────►│   "연결할래?"
    │                                   │
    │◄────────────────── SYN-ACK ──────│   "ㅇㅋ 나도 연결할래"
    │                                   │
    │── ACK ───────────────────────────►│   "확인. 이제 시작"
    │                                   │
    └─ 평문 통로 완성 ─────────────────┘

왕복 횟수의 정확한 의미

클라이언트는 SYN을 보낸 뒤 약 1 RTT에 SYN-ACK을 받습니다. 서버가 마지막 ACK(또는 함께 실린 데이터)를 받는 시점은 그보다 한쪽 방향 전파 시간만큼 뒤입니다.

  • SYN(브라우저→서버) + SYN-ACK(서버→브라우저) = 1 RTT
  • 마지막 ACK는 클라이언트가 데이터와 함께 보낼 수 있음 (지연 누적 X)

거리별 비용:

경로RTTTCP 핸드셰이크 비용
한국 ↔ 한국 (같은 IDC)5~10ms~10ms
한국 ↔ 일본30~50ms~50ms
한국 ↔ 미국 서부130~150ms~150ms
한국 ↔ 미국 동부180~220ms~220ms

이 비용은 서버 코드와 무관. 거리 자체가 만든다.


3. TLS 핸드셰이크 — 암호화 협상

TCP로 만든 통로는 평문입니다. 가운데서 누가 엿들으면 다 보이죠. HTTPS의 보안성은 TCP 위에 한 층 더 얹는 TLS 핸드셰이크에서 옵니다.

TLS 1.2 vs TLS 1.3

버전핸드셰이크 RTT비고
TLS 1.2보통 2 RTT신규 full handshake 기준. TLS 1.0/1.1은 RFC 8996으로 폐지
TLS 1.31 RTT신규 full handshake 기준
TLS 1.3 early data0-RTT 전송 가능PSK 재개에서만 가능하며 replay 위험과 서버 정책 제약이 있음

TLS 1.3 흐름

[브라우저]                            [서버]
    │                                   │
    │── ClientHello ──────────────────►│   "이런 알고리즘 지원해, 임시 키 a"
    │                                   │
    │◄── ServerHello + 인증서 ─────────│   "이거 쓰자, 내 임시 키 b, 내 신분증"
    │   + Finished                      │
    │                                   │
    │── Finished ─────────────────────►│   "확인 완료, 키 합성, 암호화 시작"
    │                                   │
    └─ 암호화 보안 통로 완성 ─────────┘

TLS 1.3에서는 클라이언트가 Finished와 애플리케이션 데이터를 같은 flight에 보낼 수 있습니다. 이후 HTTP 데이터는 협상된 키로 보호됩니다.

비유

  • TCP: 전화선 연결. "여보세요? 여보세요. 잘 들려? 응 잘 들려."
  • TLS: 통화하면서 "암호 책 페이지 맞추자. 47페이지 5번 단어부터?"

왜 분리되어 있나?

1990년대 인터넷 초기에는 암호화가 없었습니다(HTTP). 나중에 보안이 필요해지자 "TCP는 그대로, 위에 암호화 층을 얹자"는 방식으로 SSL → TLS가 추가됐습니다. 레이어 분리 덕분에 TCP는 그대로 두고 TLS만 발전시킬 수 있는 구조.

거리별 누적 핸드셰이크 비용

아래 RTT와 계산값은 연결 재사용이 없는 흐름을 설명하기 위한 가상 예시입니다. 실제 값은 사용자 ISP, 라우팅, 프록시·CDN, TLS 재개 여부에 따라 달라집니다.

한국→한국 (RTT 7ms):
   TCP 1 RTT + TLS 1.3 1 RTT = 14ms     ← 거의 무시 가능

한국→일본 (RTT 40ms):
   TCP 1 RTT + TLS 1.3 1 RTT = 80ms     ← 살짝 느낌

한국→미국 동부 (RTT 200ms):
   TCP 1 RTT + TLS 1.3 1 RTT = 400ms    ← 매우 느림
   TCP 1 RTT + TLS 1.2 2 RTT = 600ms    ← TLS 1.2면 더 느림

한국→미국 동부 + HTTP/3 0-RTT 재개:
   연결 설정 대기를 줄일 수 있지만 요청과 첫 응답의 네트워크 왕복은 남음

RTT가 커지면 새 연결의 핸드셰이크 비용도 함께 늘어납니다. 정적 자원이나 일부 동적 처리를 사용자 가까이 배치하면 이 구간을 줄일 수 있지만, 실제 개선 폭은 연결 재사용률과 서버 처리 시간을 함께 측정해야 판단할 수 있습니다.


4. "첫 바이트 도착" — 스트림이라는 사실

HTTP 응답은 한 덩어리로 한 번에 오지 않습니다. 스트림으로, 패킷이 줄줄이 옵니다.

서버 내부 타임라인:
─────────────────────────────────────────────────▶ 시간

[DB 쿼리] [auth 호출] [HTML 렌더링] [응답 시작]──[…계속 전송…]──[응답 끝]
                                       ↑                          ↑
                              첫 바이트 출발                  마지막 바이트
                              ↓                                    ↓
                         이 시점이                          이 시점이
                       TTFB의 끝                          time_total의 끝
측정값의미
time_starttransfer (curl)첫 바이트 도착 = TTFB
time_total (curl)마지막 바이트 도착
차이본문 다운로드 시간

첫 바이트가 빠르면 왜 좋은가

브라우저는 HTML을 받자마자 파싱 시작합니다. 다 받을 때까지 기다리지 않음. 첫 바이트 도착 → 즉시 HTML 파싱 → <link rel="stylesheet"> 만나면 CSS 다운로드 시작 → <script> 만나면 JS 다운로드 시작 → 화면 그리기 시작.

첫 바이트가 빨리 와야 이 체인 전체가 빨리 시작됨. 그래서 TTFB는 페이지 로딩 체감 속도의 첫 도미노입니다.


5. ping vs TTFB — 시나리오별 진단

TTFB 숫자가 비슷해 보여도 연결·서버·캐시 상태에 따라 원인이 달라질 수 있습니다. 아래 가상 시나리오는 어떤 추가 계측이 필요한지 보여 줍니다.

시나리오 A — 서버 처리 구간을 의심하는 가상 사례

[가상 측정]
ping:    7ms    ████
TTFB:    350ms  ████████████████████████████████

→ ICMP 왕복은 짧음
→ curl 누적 구간과 서버의 `Server-Timing`/APM을 함께 봐야 서버 시간을 분리 가능

진단 방향: 이 두 값만으로 서버 병목을 확정하지 않습니다. curl 구간별 시간과 서버 APM을 먼저 대조한 뒤 서버 코드, DB, 외부 API를 확인합니다.

시나리오 B — 네트워크 구간을 의심하는 가상 사례

ping:    200ms  ████████████████████
TTFB:    550ms  ████████████████████████████████████████████████████████

→ 새 TCP + TLS 1.3 연결이면 연결 설정에 약 2 RTT가 들고,
   그 뒤 요청 전송과 첫 응답 도착에도 네트워크 시간이 추가됨
→ 정확한 서버 시간은 단순 뺄셈이 아니라 서버 측 계측으로 확인

주의: ping은 ICMP 경로이고 HTTP 연결은 프록시·CDN·Anycast와 연결 재사용 상태의 영향을 받습니다. ping 값만으로 HTTPS TTFB의 하한을 계산하지 마세요.

진단 방향: 연결 설정과 전송 구간의 비중이 크다면 서버 코드 최적화만으로는 개선 폭이 작을 수 있습니다. CDN/Edge 배치, 연결 재사용과 대상 리전 변경을 각각 측정합니다.

시나리오 C — DNS 콜드 스타트를 의심하는 가상 사례

[가상 측정]
ping:    7ms
TTFB:    430ms  (첫 방문)
TTFB:    350ms  (두 번째 방문)
         ─────
        80ms 차이

두 실행의 차이만으로 DNS 원인을 확정할 수 없습니다. time_namelookup을 직접 비교하고, CDN·애플리케이션 캐시와 서버 부하도 함께 기록하세요.


6. 실전 측정 — curl 한 줄로 모든 단계 분해

다음 URL은 자리표시자입니다. 실제 측정할 서비스 주소로 바꿔 실행합니다.

curl -w '
DNS 조회:        %{time_namelookup}s
TCP 연결:        %{time_connect}s
TLS 핸드셰이크:   %{time_appconnect}s
첫 바이트까지 누적: %{time_starttransfer}s
전체 다운로드:    %{time_total}s
' -o /dev/null -s https://your-service.example/

출력 해석

아래 출력 역시 계산 방법을 보여 주는 가상 결과입니다.

DNS 조회:        0.045123s    ← DNS만의 시간
TCP 연결:        0.052456s    ← DNS + TCP까지 (누적)
TLS 핸드셰이크:   0.066789s    ← + TLS까지 (누적)
첫 바이트까지 누적: 0.350123s ← curl의 time_starttransfer
전체 다운로드:    0.412345s    ← 응답 본문까지

구간별 비용 계산:

DNS:  0.045 - 0     = 45ms
TCP:  0.052 - 0.045 = 7ms       ← RTT와 거의 같음
TLS:  0.066 - 0.052 = 14ms      ← 1 RTT (TLS 1.3)
요청~첫 바이트: 0.350 - 0.066 = 284ms ← 요청 전송, 서버·프록시 처리, 첫 바이트 반환을 모두 포함
본문: 0.412 - 0.350 = 62ms

같은 명령 두 번 돌리기

캐시 효과 분리:

# 1회차 (콜드)
curl -w "DNS:%{time_namelookup}s TTFB:%{time_starttransfer}s\n" -o /dev/null -s https://your-service.example/
# DNS:0.045s TTFB:0.430s

# 2회차 (DNS 캐시됨)
curl -w "DNS:%{time_namelookup}s TTFB:%{time_starttransfer}s\n" -o /dev/null -s https://your-service.example/
# DNS:0.000s TTFB:0.350s
#       ↑           ↑
#    DNS 경로 변화  나머지 구간도 별도 비교 필요

브라우저에서 보기

  • Chrome DevTools → Network → 임의 요청 클릭 → Timing 탭
  • "Stalled / DNS Lookup / Initial Connection / SSL / Waiting (TTFB) / Content Download" 6단계로 분해됨
  • chrome://net-export/로 NetLog를 수집해 연결과 DNS 이벤트를 확인

7. 줄이는 법 — 단계별 처방

DNS 단계

처방효과
<link rel="dns-prefetch" href="//api.example.com">다른 도메인 DNS 미리 조회
<link rel="preconnect" href="https://api.example.com">DNS + TCP + TLS까지 미리
TTL 적절히 설정너무 짧으면 매번 콜드, 너무 길면 IP 변경 시 지연
Anycast DNS (Cloudflare, Vercel)글로벌 어디서든 가까운 노드

TCP/TLS 단계

처방효과
연결 재사용같은 호스트 재요청 시 핸드셰이크를 생략할 수 있음. HTTP/1.1은 persistent connection이 기본이지만 서버·프록시 정책에 좌우됨
HTTP/2 multiplexing한 연결로 여러 요청 병렬
HTTP/3 (QUIC)TCP+TLS를 1 RTT로 합침. 0-RTT 재개 가능
TLS 1.3 지원full handshake를 1 RTT로 줄일 수 있음
0-RTT 재개replay 가능한 요청에만 서버 정책과 애플리케이션 안전성을 검토해 사용

거리 단계

처방효과
CDN / Edge 배포정적 자원은 사용자 가까운 PoP에서 응답
Edge Functions (Vercel Edge, Cloudflare Workers)동적 응답도 Edge에서 처리
읽기 전용 DB 복제본글로벌 사용자에 가까운 read replica

서버 단계 (TTFB - 네트워크 비용)

처방효과
DB 쿼리 인덱스 / N+1 제거서버 처리 구간이 클 때 우선 확인할 후보
외부 API 병렬 호출직렬 → 병렬로 RTT 합치기
캐시 (Redis, in-memory)DB hit 자체 줄이기
Streaming 응답헤더 먼저 보내고 body는 점진적 (TTFB 단축)
Server Components / SSR 최적화Next.js 등 프레임워크 패턴 적용

8. 결론 + 진단 흐름도

TTFB는 도구별 시작점이 다른 합산값입니다. curl 구간값과 서버 측 계측을 함께 봐야 병목을 분리할 수 있습니다.

                  [TTFB가 느리다는 신고]
                         │
                         ▼
              [curl 누적 구간 측정]
                         │
            ┌────────────┼────────────┐
      DNS/연결 큼   요청~응답 큼    실행별 편차 큼
            │            │              │
      resolver/거리   서버·프록시·거리   캐시·부하·라우팅
            ▼            ▼              ▼
       [서버 점검]   [거리 줄이기]   [서버 코드/DB 점검]
       - DB 쿼리     - CDN/Edge       - 캐시 도입
       - N+1         - HTTP/3         - 외부 API 병렬화
       - 외부 API    - 가까운 region   - 응답 streaming

관련 글

참고 자료

© 2026 Frank Kim. All rights reserved.