콘텐츠로 이동

CDN Cache Status 헤더는 어떻게 읽어야 할까요?

HIT이면 무조건 빠르고, MISS면 무조건 문제가 있는 걸까요? 사실은 그 한 단어만으로는 캐시가 왜 그렇게 판단했는지까지 알 수 없어요.

CDN, Cache, 그리고 Edge Delivery에서는 사용자 가까이에 복사본을 두는 큰 그림을 봤어요. 그리고 Cache-Control과 Age 헤더에서는 사본이 아직 fresh인지, Cache Key와 Vary에서는 같은 URL처럼 보여도 사본이 왜 갈라질 수 있는지 읽었죠.

이번에는 브라우저 Network 탭이나 curl -I에서 자주 보이는 이런 줄을 볼게요.

HTTP/2 200
cache-control: public, max-age=600
age: 128
cf-cache-status: HIT
vary: Accept-Encoding

처음에는 cf-cache-status: HIT만 눈에 들어와요.

"아, CDN 캐시에서 나온 거구나."

맞아요. 그런데 여기서 끝내면 아직 반쪽이에요. HIT인지 MISS인지, Age가 있는지, Cache-Control이 무엇인지, Vary가 어떤 요청 헤더를 비교하는지까지 같이 봐야 왜 이 요청이 캐시에서 끝났는지를 좁힐 수 있어요.

오늘 질문은 이거예요.

"이 응답은 캐시에 있었나요, 왜 없었나요, 있어도 그대로 써도 되는 사본이었나요?"

HTTP 캐시의 기본 판단은 RFC 9111: HTTP Caching을 바닥에 두고, 캐시가 자신이 한 일을 설명하는 표준 헤더로는 RFC 9211: Cache-Status가 있어요. 다만 실제 운영 화면에서는 CF-Cache-Status, X-Cache, X-Cache-Status처럼 제품별 헤더를 더 자주 볼 수 있어요. 예를 들어 Cloudflare는 CF-Cache-Status 값으로 HIT, MISS, BYPASS, DYNAMIC, STALE, REVALIDATED, UPDATING 같은 상태를 문서화해요.

이 글의 범위

여기서는 특정 CDN 설정 화면을 외우기보다, CDN 캐시 상태 헤더를 다른 HTTP 캐시 신호와 함께 읽는 순서를 잡아요. 상태 이름은 제품마다 다를 수 있으니, 실제 장애 분석에서는 해당 CDN 문서를 같이 확인해야 해요.


택배 상자에 찍힌 처리 도장만 보고 끝내면 부족해요

택배를 조회할 때를 떠올려볼게요.

  • 배송 완료라고 찍혀 있으면 일단 물건이 도착한 건 맞아요.
  • 하지만 어느 지점에서 처리됐는지, 언제 도착했는지, 반품이나 재배송이 있었는지는 따로 봐야 해요.
  • 집하라고 찍혀 있어도 문제가 아니라, 첫 배송이라서 아직 이동 중일 수 있어요.
  • 보류라고 찍혀 있으면 주소 문제인지, 통관 문제인지, 물량 폭주인지 추가 단서가 필요해요.

CDN 캐시 상태 헤더도 비슷해요. HIT이나 MISS처리 도장에 가까워요. 도장 자체는 중요하지만, 그 도장이 찍힌 이유까지 알려면 주변 단서를 같이 봐야 해요.

택배 조회 장면 CDN 캐시 장면
배송 상태 도장 CF-Cache-Status, X-Cache, Cache-Status
물건이 지점에 머문 시간 Age
언제까지 보관해도 되는지 Cache-Control, s-maxage, max-age
같은 물건인지 구분하는 송장 조건 cache key, Vary
보관하면 안 되는 물건 private, no-store, 인증/쿠키 영향
새 물건인지 다시 확인한 기록 ETag, If-None-Match, 304 Not Modified

핵심은 캐시 상태 헤더가 캐시가 방금 무엇을 했는지 말해주는 관측 신호라는 점이에요. 캐시 정책 자체는 Cache-Control, Vary, CDN 설정, 요청 조건이 함께 결정해요.

flowchart LR
    A[요청 도착<br/><small>URL, method, headers</small>] --> B[CDN 캐시가 key 계산]
    B --> C{저장된 사본이 있나요?}
    C -->|없음| D[MISS<br/><small>오리진으로 전달</small>]
    C -->|있음| E{그대로 써도 되나요?}
    E -->|fresh| F[HIT<br/><small>캐시에서 응답</small>]
    E -->|stale| G[재검사 / 갱신 / stale 제공]
    A --> H[정책 확인<br/><small>Cache-Control, Vary, Cookie, Authorization</small>]
    H --> B
    H --> E

이 그림에서 HITMISS는 마지막 결과예요. 그 앞에는 key 계산, 저장 가능 여부, freshness, 재검사 판단이 먼저 있어요.

상태 헤더 이름은 제품마다 달라요

브라우저나 curl에서 볼 수 있는 캐시 상태 헤더는 한 가지가 아니에요.

CF-Cache-Status: HIT
X-Cache: HIT
X-Cache-Status: MISS
Cache-Status: "ExampleCache"; hit; ttl=246

Cache-Status는 RFC 9211에 정의된 표준 헤더예요. 반면 CF-Cache-StatusX-Cache 계열은 제품이나 배포 환경에서 오래 써온 관측용 헤더에 가까워요.

헤더 읽는 감각 조심할 점
Cache-Status 표준화된 캐시 처리 설명 모든 CDN이 기본으로 붙이는 건 아니에요
CF-Cache-Status Cloudflare 캐시 처리 결과 값의 의미는 Cloudflare 문서를 기준으로 봐야 해요
X-Cache 프록시/CDN이 남긴 캐시 힌트 제품별 값과 포맷이 다를 수 있어요
X-Cache-Status 배포 환경에서 만든 상태 힌트 표준 의미라고 단정하면 안 돼요

그래서 처음 보는 헤더를 만나면 이렇게 묻는 게 좋아요.

"이 헤더는 HTTP 표준 헤더인가요, CDN 제품이 붙인 헤더인가요, 우리 프록시가 커스텀으로 붙인 헤더인가요?"

이 질문을 먼저 해야 HIT, MISS, BYPASS 같은 단어를 너무 넓게 일반화하지 않게 돼요.

먼저 여섯 가지 신호를 한 화면에서 봐요

캐시 상태를 읽을 때는 응답 헤더 하나만 보지 말고, 요청과 응답을 같이 봐야 해요.

Request URL: https://example.com/assets/app.8f31c2.css
Request Method: GET
Request Headers:
  Accept-Encoding: br, gzip

Response Headers:
  Status: 200
  Cache-Control: public, max-age=31536000, immutable
  Age: 86400
  Vary: Accept-Encoding
  Content-Encoding: br
  CF-Cache-Status: HIT

처음에는 아래 순서로 좁히면 좋아요.

순서 볼 것 묻는 질문
1 요청 method와 URL 캐시가 재사용할 수 있는 요청인가요?
2 캐시 상태 헤더 HIT, MISS, BYPASS, STALE 중 무엇처럼 보이나요?
3 Cache-Control 저장 가능하고, 얼마 동안 fresh인가요?
4 Age 공유 캐시에 이미 얼마나 있었나요?
5 Vary와 요청 헤더 같은 URL이어도 사본이 나뉘는 조건이 있나요?
6 쿠키와 인증 조건 공유 캐시에 두면 안 되는 사용자별 응답인가요?

이 순서가 중요한 이유는, MISS가 항상 "캐시가 안 된다"는 뜻이 아니기 때문이에요. 첫 요청이라서 MISS일 수 있고, key가 달라서 MISS일 수 있고, 정책상 BYPASS로 빠졌을 수도 있고, stale 사본을 재검사하느라 오리진에 갔을 수도 있어요.

상태값은 결과, 주변 헤더는 이유예요

HIT이나 MISS는 캐시가 낸 결과예요. 그 이유는 Cache-Control, Age, Vary, 요청 헤더, 쿠키, CDN 설정 쪽에서 찾아야 해요.

HIT은 "저장된 사본을 썼다"에 가까워요

가장 읽기 쉬운 장면부터 볼게요.

HTTP/2 200
cache-control: public, max-age=31536000, immutable
age: 86400
vary: Accept-Encoding
content-encoding: br
cf-cache-status: HIT

이 응답은 캐시에서 나온 정적 파일처럼 보여요.

신호 읽기
CF-Cache-Status: HIT CDN에 저장된 사본이 사용됐어요
Age: 86400 공유 캐시 안에서 하루 정도 나이를 먹었어요
max-age=31536000 freshness lifetime이 아주 길어요
immutable URL이 바뀌지 않는 한 내용도 바뀌지 않는다는 강한 힌트예요
Vary: Accept-Encoding 압축 방식별 사본이 나뉠 수 있어요

여기서는 Age가 크다고 해서 바로 오래된 나쁜 응답이라고 보면 안 돼요. 해시가 붙은 정적 파일이고 max-age가 1년이라면, 하루 된 사본은 여전히 fresh일 수 있어요.

flowchart LR
    A[오리진<br/><small>app.8f31c2.css</small>] --> B[CDN 저장]
    B --> C[첫 요청 이후 Age 증가]
    C --> D[다음 사용자 요청]
    D --> E[HIT<br/><small>fresh 사본 전달</small>]

이 그림은 HIT이 "오리진을 매번 가지 않았다"는 뜻에 가깝다는 걸 보여줘요. 하지만 HIT이 항상 올바른 응답이라는 보장은 아니에요. cache key나 Vary가 잘못됐으면 잘못된 사본도 HIT으로 나갈 수 있어요.

MISS는 "이번 요청에는 바로 쓸 사본이 없었다"에 가까워요

이번에는 첫 요청 장면이에요.

HTTP/2 200
cache-control: public, max-age=600
cf-cache-status: MISS

이건 꼭 장애가 아니에요. CDN 엣지에 아직 사본이 없어서 오리진에 다녀온 것일 수 있어요. 같은 조건으로 다시 요청하면 그때는 HIT이 될 수도 있죠.

sequenceDiagram
    participant U1 as 첫 번째 사용자
    participant C as CDN
    participant O as 오리진
    participant U2 as 다음 사용자

    U1->>C: GET /style.css
    C->>O: MISS, 원본 요청
    O-->>C: 200 OK + Cache-Control
    C-->>U1: MISS 응답 전달
    U2->>C: GET /style.css
    C-->>U2: HIT, 저장된 사본 전달

MISS를 봤다면 아래를 같이 확인해요.

확인할 것 왜 보나요?
첫 요청인지 새 배포, 새 URL, 새 엣지 위치에서는 자연스러운 MISS일 수 있어요
Cache-Control 오리진 응답이 저장 가능한지 봐야 해요
Set-Cookie 쿠키가 붙으면 공유 캐시가 우회될 수 있어요
Vary와 요청 헤더 다른 언어, 압축, 쿠키 조건 때문에 다른 key가 됐을 수 있어요
CDN 규칙 특정 path나 status code를 캐시하지 않도록 설정됐을 수 있어요

MISS를 곧바로 'CDN이 고장났다'로 읽지 마세요

MISS는 이번 요청에서 저장된 사본을 바로 쓰지 못했다는 신호예요. 첫 요청, 새 key, 만료 후 재검사, 정책 우회가 모두 비슷하게 오리진 경유로 보일 수 있어요.

BYPASS와 DYNAMIC은 "캐시를 쓰지 않기로 했다"에 가까워요

이번에는 응답이 캐시에 저장되기 어려운 장면이에요.

HTTP/2 200
cache-control: private, no-store
set-cookie: session=...
cf-cache-status: BYPASS

또는 이런 식으로 볼 수도 있어요.

HTTP/2 200
cache-control: private, max-age=0
cf-cache-status: DYNAMIC

이런 값은 제품별 의미가 다르지만, 운영 감각으로는 캐시에서 꺼낸 사본이 아니라 오리진이나 동적 처리 경로로 갔다고 먼저 읽으면 좋아요.

신호 먼저 의심할 장면
private 사용자별 응답이라 공유 캐시에 두기 어려워요
no-store 저장 자체를 피해야 하는 응답이에요
max-age=0 바로 stale로 보고 재검사해야 할 수 있어요
Set-Cookie 사용자 상태를 바꾸는 응답일 수 있어요
Authorization 요청 헤더 인증된 사용자 요청일 수 있어요

여기서 중요한 반전이 있어요.

"캐시가 안 됐으니 나쁜 설정이다"

항상 그렇지는 않아요. 로그인 페이지, 장바구니, 결제 응답, 개인화 API는 공유 캐시에 저장되지 않는 편이 맞을 수 있어요. 캐시 상태 헤더는 성능 점수표가 아니라 정책 판단의 흔적이에요.

EXPIRED, STALE, REVALIDATED, UPDATING은 "오래된 사본을 어떻게 처리했나"를 봐야 해요

캐시에는 사본이 있었지만 freshness lifetime이 끝난 장면도 있어요.

HTTP/2 200
cache-control: public, max-age=300
age: 300
etag: "hero-v12"
cf-cache-status: REVALIDATED

이런 응답은 저장된 사본이 있었고, CDN이 오리진에 다시 확인했을 가능성을 떠올리게 해요. 오리진이 304 Not Modified로 "그 사본 그대로 써도 돼요"라고 확인해주면, 캐시는 본문을 다시 받지 않고 기존 사본을 내보낼 수 있어요.

sequenceDiagram
    participant B as 브라우저
    participant C as CDN 캐시
    participant O as 오리진

    B->>C: GET /hero.jpg
    C-->>C: 저장된 사본 발견<br/>하지만 stale
    C->>O: If-None-Match: "hero-v12"
    alt 바뀌지 않음
        O-->>C: 304 Not Modified
        C-->>B: 기존 사본 전달
    else 바뀜
        O-->>C: 200 OK + 새 본문
        C-->>B: 새 사본 전달
    end

반대로 STALE이나 UPDATING류의 값은 stale 사본을 어떻게 다뤘는지를 봐야 해요. 어떤 CDN은 오리진이 잠시 닿지 않을 때 오래된 사본을 대신 줄 수 있고, 어떤 설정에서는 stale 사본을 주면서 백그라운드로 갱신할 수 있어요. 이 영역은 제품과 설정 차이가 크기 때문에, 상태값 이름만 외우지 말고 아래 질문을 같이 남겨야 해요.

질문 왜 중요할까요?
stale 사본을 사용자에게 줬나요? 사용자는 빠르게 받았지만 오래된 값을 봤을 수 있어요
오리진 재검사를 기다렸나요? 지연이 늘어날 수 있어요
백그라운드 갱신이 켜져 있나요? 첫 요청만 갱신 비용을 치르고 뒤 요청은 빠를 수 있어요
must-revalidate가 있나요? stale 사본을 그냥 쓰지 못하게 할 수 있어요

같은 HIT이어도 캐시 키가 틀리면 위험해요

HIT이 나왔는데 사용자에게 엉뚱한 언어가 보인다고 해볼게요.

GET /guide HTTP/2
accept-language: en-US,en;q=0.9

HTTP/2 200
cache-control: public, max-age=600
content-language: ko
cf-cache-status: HIT

여기서 문제는 HIT이 아니라 무엇을 같은 사본으로 봤는지일 수 있어요. 언어별로 본문이 달라진다면 Vary: Accept-Language나 CDN의 커스텀 cache key가 그 차이를 반영해야 해요.

flowchart TD
    A[한국어 요청] --> B[한국어 응답 저장]
    B --> C[캐시 HIT]
    D[영어 요청] --> C
    C --> E[영어 사용자에게<br/>한국어 사본 전달]
    F[Vary / cache key 누락] --> C

    style E stroke:#ef4444,stroke-width:2px

이런 장면에서는 캐시 상태 헤더만 보면 오히려 속을 수 있어요. HIT은 캐시가 사본을 찾았다는 뜻이지, 그 사본이 이 사용자에게 맞는지까지 대신 보증하지 않아요.

브라우저 캐시와 CDN 캐시를 섞어 읽지 마세요

Network 탭에는 이런 표시도 나와요.

Name           Status   Size             Time
app.css        200      (memory cache)    0 ms
logo.svg       200      (disk cache)      1 ms
api/items      200      3.8 KB            92 ms

(memory cache)(disk cache)는 브라우저 쪽 캐시 표시예요. CDN의 CF-Cache-Status: HIT과는 관측 위치가 달라요.

위치 캐시 신호 읽는 감각
브라우저 (memory cache), (disk cache) 사용자 기기 안에서 재사용
CDN / 공유 캐시 Age, CF-Cache-Status, X-Cache, Cache-Status 중간 엣지나 프록시에서 재사용
오리진 access log, request id 실제 원본 서버까지 도착했는지

그래서 "내 브라우저에서는 빠른데 다른 사용자는 느리다" 같은 문제에서는 브라우저 캐시를 끄거나 hard reload, curl -H 'Cache-Control: no-cache', 다른 지역/네트워크 테스트처럼 관측 조건을 나눠야 해요. 단, 요청에 Cache-Control: no-cache를 붙이면 캐시 동작 자체가 달라질 수 있으니, 평소 사용자 요청과 디버깅 요청을 구분해서 봐야 해요.

잘못 읽기 쉬운 함정

HIT이면 항상 최신이라고 보기

HIT은 캐시에서 나왔다는 뜻에 가까워요. 최신인지 아닌지는 Cache-Control, Age, 재검사 정책, purge 여부를 같이 봐야 해요.

MISS이면 캐시 설정이 실패했다고 보기

첫 요청, 새 배포 파일, 새 query, 새 언어 조건, 새 엣지 위치에서는 MISS가 자연스러울 수 있어요. 같은 조건의 두 번째 요청도 계속 MISS인지 봐야 해요.

Age가 없으면 CDN을 안 탔다고 단정하기

제품별로 Age를 붙이는 조건이 다를 수 있어요. 예를 들어 어떤 CDN은 캐시에서 실제로 제공한 응답에만 Age를 붙이고, MISS나 동적 응답에는 붙이지 않을 수 있어요. Age 없음 하나로 경로 전체를 단정하지 마세요.

벤더 상태값을 모든 CDN에 똑같이 적용하기

BYPASS, DYNAMIC, UPDATING 같은 이름은 제품별 의미와 조건이 달라요. 글에서는 읽기 감각을 잡지만, 실제 운영에서는 해당 CDN 문서와 설정을 같이 봐야 해요.

HIT만 높이면 무조건 좋다고 보기

히트율은 중요하지만, 사용자별 응답까지 공유 캐시에 섞이면 더 큰 문제가 돼요. 캐시는 빠른 것도 중요하지만, 맞는 사본을 맞는 사용자에게 주는 것이 먼저예요.

장애나 이상 증상을 만나면 이렇게 좁혀봐요

flowchart TD
    A[캐시 상태가 이상해 보여요] --> B{계속 MISS인가요?}
    B -->|예| C[Cache-Control, Set-Cookie, Authorization, CDN 규칙 확인]
    B -->|아니오| D{HIT인데 내용이 이상한가요?}
    D -->|예| E[Vary, cache key, 언어/쿠키/압축 조건 확인]
    D -->|아니오| F{오래된 내용인가요?}
    F -->|예| G[Age, max-age, purge, revalidation 확인]
    F -->|아니오| H[브라우저 캐시와 CDN 캐시를 분리해서 재현]
    C --> I[같은 조건으로 두 번째 요청 비교]
    E --> I
    G --> I

실전에서는 아래 질문을 남기면 좋아요.

질문 답을 찾을 곳
캐시 상태 헤더는 누가 붙였나요? CDN 문서, 프록시 설정, 응답 헤더 이름
같은 요청을 두 번 보냈을 때 상태가 바뀌나요? curl -I 반복, Network 탭 재시도
저장 가능한 응답인가요? Cache-Control, status code, method
같은 사본으로 봐도 되는 요청인가요? URL, query, Vary, 커스텀 cache key
사용자별 응답이 섞일 위험이 있나요? Cookie, Authorization, private, no-store
stale 사본이 사용자에게 나갔나요? Age, 상태값, 재검사 로그, CDN 설정

자, 정리해볼까요?

오늘 우리가 배운 것

  • CDN 캐시 상태 헤더는 캐시가 이번 요청을 어떻게 처리했는지 보여주는 관측 신호예요.
  • HIT은 저장된 사본을 썼다는 뜻에 가깝지만, 그 사본이 최신인지 또는 이 요청에 맞는지는 별도로 봐야 해요.
  • MISS는 이번 요청에 바로 쓸 사본이 없었다는 뜻에 가깝고, 첫 요청이나 새 cache key에서는 자연스러울 수 있어요.
  • BYPASS, DYNAMIC, STALE, REVALIDATED, UPDATING 같은 값은 제품별 의미가 크기 때문에 해당 CDN 문서와 함께 읽어야 해요.
  • 캐시 상태 헤더는 Cache-Control, Age, Vary, 요청 헤더, 쿠키, 인증 조건과 같이 봐야 이유가 보여요.
  • 브라우저 캐시와 CDN 캐시는 관측 위치가 다르므로 (memory cache) 표시와 CDN 상태 헤더를 섞어 읽으면 안 돼요.

캐시 상태 헤더는 캐시 디버깅의 결론이 아니라 출발점이에요. HIT인지 MISS인지 본 다음, "왜 그렇게 됐을까?"를 주변 헤더와 요청 조건으로 좁혀가는 게 핵심이에요.

이어서 보면 좋은 글

댓글