실제 터미널 캡처: curl로 ainewsnote.com 응답 헤더를 보니 홈은 cf-cache-status DYNAMIC, 이미지는 MISS였고 헤더 딕셔너리에서 cf-cache-status를 찾다 KeyError가 난 화면과 글 제목

CF-Cache-Status DYNAMIC KeyError? 캐시 헤더를 계층별로 읽기

CF-Cache-Status와 본문의 생성 주석은 표현이 비슷해서, 홈의 PageSpeed를 보다가 둘을 같이 읽으면 서로 다른 계층의 신호를 같은 뜻으로 받아들이기 쉽습니다. 웹 응답을 점검할 때 페이지가 열리는지와 어느 계층이 캐시했는지는 따로 따져야 하는 질문입니다. 이번 응답의 DYNAMIC과 WP Super Cache 주석을 갈라 읽는 것을 목표로, 캐시 점검 스크립트를 운영에 넣을 수 있는지 Debian에서 시험했습니다. 워드프레스 홈 응답을 받아 Cloudflare 상태를 해석하는 코드가 실제 응답에서도 끝까지 도는지를 재현·해결·점검 스크립트로 실행해 기록했습니다.

Debian에서 https://ainewsnote.com/ 홈을 받아, HIT와 MISS와 EXPIRED와 BYPASS만 가진 파이썬 맵이 DYNAMIC에서 KeyError로 멈추는 장면부터 재현했습니다. 그다음 그 키를 맵에 넣고 주석과 LiteSpeed 헤더를 나눠 읽는 방법, 닫힌 맵 대신 분기로 상태를 남기는 방법을 실행 결과와 함께 비교합니다.

HIT·MISS·EXPIRED·BYPASS만 아는 맵이 DYNAMIC에서 멈춤

재현 환경은 Debian GNU/Linux 13, Python 3.13.5, curl 8.14.1입니다. 표의 CPU 8과 RAM_TOTAL_GB 15는 실행 당시 환경을 적어 둔 값일 뿐이고, 캐시 상태를 해석하는 이번 실험에서는 처리량이나 서버 사양으로 바꿔 읽지 않습니다. 운영 환경의 결과와 맞대어 볼 때 실행 도구와 요청 조건을 먼저 맞추는 기준으로 씁니다.

항목값
실행일2026-10-14
OSDebian GNU/Linux 13 (trixie)
커널Linux 6.12.94+ x86_64
PythonPython 3.13.5
curlcurl 8.14.1
CPU8
RAMRAM_TOTAL_GB 15

요청 결과를 비교하기 전에 실행 환경부터 남깁니다. 도구가 바뀐 시점과 응답이 바뀐 시점을 나중에 가르려면 이런 기록이 있어야 합니다. 아래 명령은 날짜, 운영체제, 커널과 함께 Python과 curl 버전을 찍고, 뒤의 실행 로그에는 CPU 8과 RAM_TOTAL_GB 15도 같은 순서로 남습니다.

date +%Y-%m-%d
. /etc/os-release && printf '%s\n' "$PRETTY_NAME"
uname -srm
python3 --version
curl --version | head -n 1
nproc
free -g | awk 'NR==2{print "RAM_TOTAL_GB", $2}'

홈은 HEAD와 GET으로 한 번씩 받았습니다. User-Agent는 Mozilla/5.0, 제한 시간은 20초입니다. 헤더 파일에는 상태 줄과 캐시 판정에 쓰는 이름만 남기고, 그 헤더와 본문 주석을 찍은 다음 닫힌 맵으로 상태를 조회합니다.

curl -fsS -I -A Mozilla/5.0 --max-time 20 -D tmp/a.head -o /dev/null "$SITE/"
curl -fsS -D tmp/a.headers -o tmp/a.body -A Mozilla/5.0 --max-time 20 "$SITE/"
python3 show_signals.py tmp/a.head tmp/a.headers tmp/a.body
python3 naive_cf_status.py tmp/a.headers

조회에 쓴 맵은 아래 스크립트로, HIT, MISS, EXPIRED, BYPASS만 키로 갖습니다. 헤더 이름은 소문자로 맞춰 비교하지만, 읽어 낸 상태 값은 그대로 딕셔너리에 넣어 찾습니다. 응답이 멀쩡해도 목록 밖의 DYNAMIC이 오면 마지막 조회에서 KeyError가 나는 구조입니다. 네트워크 실패와 상태 해석 실패가 다른 문제라는 것을 보여 주려고 일부러 이 코드에서 시작했습니다.

import pathlib
import sys

text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
status = ""
for raw in text.splitlines():
    line = raw.strip()
    if line.lower().startswith("cf-cache-status:"):
        status = line.split(":", 1)[1].strip()
labels = {
    "HIT": "edge-cached",
    "MISS": "edge-miss",
    "EXPIRED": "edge-expired",
    "BYPASS": "edge-bypass",
}
print("cf-cache-status", status)
# status가 labels에 없으면 KeyError
print(labels[status])

환경 확인부터 실패 지점까지가 한 실행 기록에 이어져 있습니다. HEAD와 GET은 둘 다 HTTP/2 200을 돌려줬고 캐시 상태도 똑같이 DYNAMIC입니다. 본문 주석을 찍은 뒤 파이썬 조회에서 KeyError가 나니, 응답을 못 받은 상황과는 분명히 다릅니다. 점검 작업의 실패 알림에 응답 상태와 파서 예외를 함께 남겨 두면 원인을 쫓기가 훨씬 수월합니다.

$ export LC_ALL=C PYTHONDONTWRITEBYTECODE=1
$ date +%Y-%m-%d
2026-10-14
$ . /etc/os-release && printf '%s\n' "$PRETTY_NAME"
Debian GNU/Linux 13 (trixie)
$ uname -srm
Linux 6.12.94+ x86_64
$ python3 --version
Python 3.13.5
$ curl --version | head -n 1
curl 8.14.1 (x86_64-pc-linux-gnu) libcurl/8.14.1 OpenSSL/3.5.7 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 nghttp3/1.8.0 librtmp/2.3 OpenLDAP/2.6.10
$ nproc
8
$ free -g | awk 'NR==2{print "RAM_TOTAL_GB", $2}'
RAM_TOTAL_GB 15
$ curl -fsS -I -A Mozilla/5.0 --max-time 20 -D tmp/a.head -o /dev/null "$SITE/"
$ curl -fsS -D tmp/a.headers -o tmp/a.body -A Mozilla/5.0 --max-time 20 "$SITE/"
$ keep_headers tmp/a.head
$ keep_headers tmp/a.headers
$ cat > show_signals.py << 'PY'
$ python3 show_signals.py tmp/a.head tmp/a.headers tmp/a.body
HEAD status HTTP/2 200
HEAD cache-control max-age=3, must-revalidate
HEAD server cloudflare
HEAD vary Accept-Encoding,Cookie
HEAD cf-cache-status DYNAMIC
HEAD content-encoding absent
HEAD content-type text/html; charset=UTF-8
HEAD age absent
HEAD x-litespeed-cache absent
HEAD x-litespeed-cache-control absent
HEAD x-lsadc-cache absent
HEAD x-qc-cache absent
HEAD x-qc-pop absent
GET status HTTP/2 200
GET cache-control max-age=3, must-revalidate
GET server cloudflare
GET vary Accept-Encoding,Cookie
GET cf-cache-status DYNAMIC
GET content-encoding absent
GET content-type text/html; charset=UTF-8
GET age absent
GET x-litespeed-cache absent
GET x-litespeed-cache-control absent
GET x-lsadc-cache absent
GET x-qc-cache absent
GET x-qc-pop absent
comment <!-- Dynamic page generated in 0.149 seconds. -->
comment <!-- Cached page generated by WP-Super-Cache on 2026-10-14 14:45:30 -->
comment <!-- super cache -->
$ cat > naive_cf_status.py << 'PY'
$ python3 naive_cf_status.py tmp/a.headers
cf-cache-status DYNAMIC
Traceback (most recent call last):
  File "/home/user/wordpress/run/naive_cf_status.py", line 18, in <module>
    print(labels[status])
          ~~~~~~^^^^^^^^
KeyError: 'DYNAMIC'

정상 HTTP 응답을 받은 뒤 상태 조회에서 멈춘 터미널입니다. HEAD와 GET의 HTTP/2 200만 검사하는 점검이라면 이 파서 실패를 그냥 지나칩니다. 거꾸로 KeyError만 보고 웹사이트 장애로 처리하면 받은 응답의 상태를 잘못 전하게 됩니다. 운영 알림에는 요청 결과와 점검 코드의 처리 결과가 따로 찍혀야 합니다.

워드프레스 캐시 헤더 실행 화면: DYNAMIC 헤더를 처리한 코드에서 KeyError: DYNAMIC이 발생합니다
2026-10-14 14:46:08 KST, Debian 13 — DYNAMIC 헤더를 처리한 코드에서 KeyError: DYNAMIC이 발생합니다.

화면에는 naive_cf_status.py가 작업 폴더의 긴 경로로 찍혔고, 본문 트레이스백은 그 경로를 /home/user/wordpress로 짧게 고친 것 말고는 같습니다.

조회 직전 줄은 cf-cache-status DYNAMIC이고, 트레이스백의 예외에도 같은 키가 적혀 있습니다. HEAD와 GET의 상태 줄이 모두 HTTP/2 200이니 이 실패는 상태 사전에서 시작됐습니다. server는 cloudflare, cache-control은 max-age=3, must-revalidate, vary는 Accept-Encoding,Cookie입니다. content-encoding, age, LiteSpeed 계열 헤더는 absent로 찍혔으니, 출력에 실제로 나온 값과 헤더가 없었다는 기록은 나눠서 다룹니다.

본문에서 골라낸 comment 줄은 Dynamic page generated, Cached page generated by WP-Super-Cache, super cache 세 개입니다. 시각과 생성 초는 출력 블록의 comment 줄에 있습니다. 같은 출력에 Dynamic Page Served, Compression, LiteSpeed Cache에 해당하는 comment 줄은 없습니다.

Cloudflare 문서의 DYNAMIC은 요청 시점의 비대상 판정

파이썬 딕셔너리는 없는 키를 찾으면 KeyError를 냅니다. 이번 맵에는 적중, 미스, 만료, 우회만 있고, Cloudflare 캐시 응답 문서의 DYNAMIC은 그 네 키에 없습니다. naive_cf_status.py는 본문을 열지 않기 때문에 주석을 읽기 전에 조회가 멈춥니다. 그 앞에 주석이 찍혀 있는 것은 show_signals.py가 입력을 먼저 출력했기 때문입니다.

같은 문서는 DYNAMIC을, 요청 시점에 자산이 캐시 대상이 아니라고 보고 캐시 조회 없이 오리진 웹 서버로 보낸 상태라고 설명합니다. 흔한 경우로는 기본 캐시 확장자가 아닌 HTML이면서 캐시 규칙도 없는 요청, Bypass cache 규칙, Development Mode를 듭니다. BYPASS는 응답 시점의 결정으로 따로 적혀 있습니다. 처음에는 캐시 대상이던 요청에 대해 오리진 응답이나 헤더가 저장하지 말라고 한 경우입니다. 이번 HEAD와 GET의 상태 값은 DYNAMIC이고, cache-control의 max-age=3은 그 응답에 실린 지시입니다. DYNAMIC이 구체적으로 어느 경우인지는 캐시 응답 문서의 요청 시점 조건에 비춰 읽어야 합니다.

문서에 따르면 Cloudflare는 Age를 HIT, STALE, UPDATING 응답에 붙이고 DYNAMIC에는 붙이지 않습니다. 오리진이 캐시할 수 없는 응답에 Age를 넣으면 그 값이 클라이언트까지 넘어갈 수 있다는 설명도 있습니다. 이번 HEAD와 GET의 age 줄은 absent입니다.

DYNAMIC 키를 넣고 WP Super Cache 주석은 따로 읽기

맵에 DYNAMIC을 넣어 라벨을 request-time-not-eligible로 두고, 같은 응답의 본문 주석과 LiteSpeed 계열 헤더를 따로 찍으면 풀립니다. 해결 스크립트는 홈을 다시 GET으로 받아 헤더와 본문을 한 응답에서 저장합니다. 따로 받은 본문을 섞으면 요청 시점이 다른 신호가 한데 묶일 수 있어서, 수집 단계에서부터 짝을 맞춰 둡니다. 아래 명령은 헤더 파일과 본문 파일을 함께 파이썬에 넘겨 그 짝을 지킵니다.

curl -fsS -D tmp/b.headers -o tmp/b.body -A Mozilla/5.0 --max-time 20 "$SITE/"
python3 fixed_cf_status.py tmp/b.headers tmp/b.body

요청 명령 뒤에는 헤더와 본문을 각각 읽는 구현이 옵니다. DYNAMIC을 넣은 맵은 request-time-not-eligible을 돌려주고, 오리진 판단은 본문 주석에서 따로 가져옵니다. LiteSpeed 계열 헤더가 있는지도 별도 항목으로 찍으니 CDN 상태와 오리진 판정이 섞이지 않습니다. 다만 이 구현도 키를 직접 조회하는 방식이라, 목록 밖의 상태까지 받아 내려면 뒤에 나오는 분기 방식이 필요합니다.

import pathlib
import sys

NAMES = [
    "cache-control",
    "server",
    "vary",
    "cf-cache-status",
    "content-encoding",
    "age",
    "x-litespeed-cache",
    "x-litespeed-cache-control",
    "x-lsadc-cache",
    "x-qc-cache",
]
COMMENT_NEEDLES = [
    "Cached page generated by WP-Super-Cache",
    "Dynamic page generated",
    "super cache",
    "Compression =",
    "LiteSpeed Cache",
    "Dynamic Page Served",
]


def values_for(text, name):
    found = []
    prefix = name + ":"
    for raw in text.splitlines():
        line = raw.strip()
        if line.lower().startswith(prefix):
            found.append(line.split(":", 1)[1].strip())
    return found


header_text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
body = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8", errors="replace")
status_values = values_for(header_text, "cf-cache-status")
status = status_values[0] if status_values else ""
labels = {
    "HIT": "edge-cached",
    "MISS": "edge-miss",
    "EXPIRED": "edge-expired",
    "BYPASS": "edge-bypass",
    "DYNAMIC": "request-time-not-eligible",
}
print("cdn_label", labels[status])
print("cf-cache-status", status)
for name in NAMES:
    found = values_for(header_text, name)
    if found:
        for value in found:
            print("header", name, value)
    else:
        print("header", name, "absent")
for raw in body.splitlines():
    line = raw.strip()
    if any(needle in line for needle in COMMENT_NEEDLES) and len(line) <= 240:
        print("comment", line)
origin = (
    "wp-super-cache"
    if "Cached page generated by WP-Super-Cache" in body
    else "no-wp-super-cache-comment"
)
lite = "present" if values_for(header_text, "x-litespeed-cache") else "absent"
print(f"OK cdn={status} origin={origin} litespeed_header={lite}")

고친 스크립트를 돌린 결과입니다. 앞에서 났던 KeyError 대신 cdn_label과 각 헤더가 찍히고 마지막 OK 줄까지 갑니다. 본문에는 WP Super Cache 생성 주석이 있고 X-LiteSpeed-Cache 헤더는 absent입니다. 성공을 판단할 때는 예외가 사라졌다는 것과, 각 캐시 신호가 어느 입력에서 나왔는지를 같이 봐야 합니다.

$ export LC_ALL=C PYTHONDONTWRITEBYTECODE=1
$ curl -fsS -D tmp/b.headers -o tmp/b.body -A Mozilla/5.0 --max-time 20 "$SITE/"
$ keep_headers tmp/b.headers
$ cat > fixed_cf_status.py << 'PY'
$ python3 fixed_cf_status.py tmp/b.headers tmp/b.body
cdn_label request-time-not-eligible
cf-cache-status DYNAMIC
header cache-control max-age=3, must-revalidate
header server cloudflare
header vary Accept-Encoding,Cookie
header cf-cache-status DYNAMIC
header content-encoding absent
header age absent
header x-litespeed-cache absent
header x-litespeed-cache-control absent
header x-lsadc-cache absent
header x-qc-cache absent
comment <!-- Dynamic page generated in 0.149 seconds. -->
comment <!-- Cached page generated by WP-Super-Cache on 2026-10-14 14:45:30 -->
comment <!-- super cache -->
OK cdn=DYNAMIC origin=wp-super-cache litespeed_header=absent

DYNAMIC을 넣은 뒤 마지막 OK 줄까지 찍힌 화면입니다. cdn=DYNAMIC과 origin=wp-super-cache가 한 줄에 있지만, 둘은 서로 다른 입력에서 나온 판정입니다. litespeed_header=absent까지 독립 항목으로 남겨 둬야 다른 캐시 제품의 적중으로 잘못 읽지 않습니다. 이 형태로 남기면 운영 로그에서도 어느 계층의 신호가 바뀌었는지 금방 찾습니다.

워드프레스 캐시 헤더 실행 화면: 상태 해석 결과로 cdn=DYNAMIC과 원본의 wp-super-cache를 보고합니다
2026-10-14 14:46:15 KST, Debian 13 — 상태 해석 결과로 cdn=DYNAMIC과 원본의 wp-super-cache를 보고합니다.

해결 스크립트의 성공 줄은 OK cdn=DYNAMIC origin=wp-super-cache litespeed_header=absent이고 cdn_label은 request-time-not-eligible입니다. origin은 본문에 Cached page generated by WP-Super-Cache가 있으면 wp-super-cache가 됩니다. litespeed_header는 X-LiteSpeed-Cache 줄이 있으면 present인데, 이번 출력은 absent입니다.

기존 딕셔너리 조회를 그대로 두고 이번 상태 하나만 더하면 KeyError 없이 끝나고, 엣지 상태, WP Super Cache 주석, X-LiteSpeed-Cache 유무도 한 번의 실행에서 갈라집니다. 한계도 분명합니다. 맵에 없는 다른 CF-Cache-Status를 만나면 다시 KeyError가 나는데, 문서의 STALE, REVALIDATED, UPDATING, NONE/UNKNOWN은 이번 맵에 없습니다. 주석 문자열이 바뀌면 origin 판정 문자열도 같이 고쳐야 하고, 이번 판정은 출력 블록의 OK 줄에 남아 있습니다.

목록 밖 상태는 분기로 받고 주석·압축·LiteSpeed 헤더를 다시 대조

아래 코드의 if와 elif와 else는 목록에 없는 상태를 unlisted로 받아 냅니다. 점검 스크립트는 그 분기에 더해 생성 주석을 두 번 비교하고, 압축을 켠 HEAD와 압축을 요청하지 않은 HEAD의 content-encoding을 나눠 보고, 플러그인 저장소의 wp-cache-phase2.php에서 주석 문자열을 찾습니다. 두 GET 사이의 대기는 sleep 2입니다.

curl -fsS -D tmp/c.headers -o tmp/c.body -A Mozilla/5.0 --max-time 20 "$SITE/"
curl -fsS -A Mozilla/5.0 --max-time 20 -o tmp/c1.body "$SITE/"
sleep 2
curl -fsS -A Mozilla/5.0 --max-time 20 -o tmp/c2.body "$SITE/"
curl -fsS -I --compressed -A Mozilla/5.0 --max-time 20 -D tmp/c.compressed.headers -o /dev/null "$SITE/"
curl -fsS -I -A Mozilla/5.0 --max-time 20 -D tmp/c.plain.headers -o /dev/null "$SITE/"
curl -fsS -L --max-time 20 -o tmp/phase2.php https://raw.githubusercontent.com/Automattic/wp-super-cache/master/wp-cache-phase2.php
python3 alt_cf_status.py tmp/c.headers tmp/c1.body tmp/c2.body tmp/c.compressed.headers tmp/c.plain.headers tmp/phase2.php

파이썬 코드는 요청 수집과 상태 해석을 갈라서 읽도록 짰습니다. 모르는 상태는 unlisted로 남겨 조회 예외 없이 다음 비교로 넘어갑니다. 생성 주석 비교는 match, differ, missing으로 나누고, 압축 요청 여부에 따른 헤더 값도 따로 찍습니다. 원문과 해석 결과를 같이 남겨 두면 해석 규칙을 바꾼 뒤에도 저장해 둔 응답으로 다시 점검할 수 있습니다.

import pathlib
import sys

header_text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8", errors="replace")
body1 = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8", errors="replace")
body2 = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8", errors="replace")
compressed = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8", errors="replace")
plain = pathlib.Path(sys.argv[5]).read_text(encoding="utf-8", errors="replace")
phase2 = pathlib.Path(sys.argv[6]).read_text(encoding="utf-8", errors="replace")


def first_value(text, name):
    prefix = name.lower() + ":"
    for raw in text.splitlines():
        line = raw.strip()
        if line.lower().startswith(prefix):
            return line.split(":", 1)[1].strip()
    return "absent"


def cache_comment(body):
    marker = "Cached page generated by WP-Super-Cache"
    for raw in body.splitlines():
        line = raw.strip()
        if marker in line and len(line) <= 240:
            return line
    return ""


status = first_value(header_text, "cf-cache-status")
if status == "HIT":
    label = "edge-cached"
elif status == "MISS":
    label = "edge-miss"
elif status == "EXPIRED":
    label = "edge-expired"
elif status == "BYPASS":
    label = "edge-bypass"
elif status == "DYNAMIC":
    label = "request-time-not-eligible"
else:
    label = "unlisted"
print("alt_label", label)
print("cf-cache-status", status)

left = cache_comment(body1)
right = cache_comment(body2)
print("comment_1", left)
print("comment_2", right)
if left and left == right:
    compare = "match"
elif left or right:
    compare = "differ"
else:
    compare = "missing"
print("comment_compare", compare)

enc_c = first_value(compressed, "content-encoding")
enc_p = first_value(plain, "content-encoding")
print("compressed content-encoding", enc_c)
print("plain content-encoding", enc_p)
print("compressed cf-cache-status", first_value(compressed, "cf-cache-status"))
print("plain cf-cache-status", first_value(plain, "cf-cache-status"))

lite_names = ["x-litespeed-cache", "x-lsadc-cache", "x-qc-cache"]
lite = "absent"
for name in lite_names:
    value = first_value(header_text, name)
    print("header", name, value)
    if value != "absent":
        lite = "present"

needles = [
    "if ( $cache_enabled || $super_cache_enabled )",
    "Cached page generated by WP-Super-Cache on",
    "Live page served on",
    "Dynamic page generated in",
    "wp_cache_add_to_buffer( $buffer, 'super cache' )",
    "Compression = gzip",
]
for index, raw in enumerate(phase2.splitlines(), start=1):
    if any(needle in raw for needle in needles):
        print(f"phase2 {index}:{raw.strip()[:220]}")

dynamic_comment = "yes" if "Dynamic page generated" in body1 else "no"
print("body_dynamic_page_generated", dynamic_comment)
print(
    "OK"
    f" alt_label={label}"
    f" comment_compare={compare}"
    f" compressed_encoding={enc_c}"
    f" plain_encoding={enc_p}"
    f" litespeed_header={lite}"
)

분기 처리와 보조 요청을 같이 돌린 기록입니다. 생성 주석 비교는 match이고, 압축을 요청한 HEAD에는 zstd가 나옵니다. 압축을 요청하지 않은 HEAD는 content-encoding이 absent이지만 양쪽 CF-Cache-Status는 모두 DYNAMIC입니다. 뒤에 붙은 저장소 검색 결과는 본문 주석 문구를 맞춰 보려는 자료입니다.

$ export LC_ALL=C PYTHONDONTWRITEBYTECODE=1
$ curl -fsS -D tmp/c.headers -o tmp/c.body -A Mozilla/5.0 --max-time 20 "$SITE/"
$ keep_headers tmp/c.headers
$ curl -fsS -A Mozilla/5.0 --max-time 20 -o tmp/c1.body "$SITE/"
$ sleep 2
$ curl -fsS -A Mozilla/5.0 --max-time 20 -o tmp/c2.body "$SITE/"
$ curl -fsS -I --compressed -A Mozilla/5.0 --max-time 20 -D tmp/c.compressed.headers -o /dev/null "$SITE/"
$ keep_headers tmp/c.compressed.headers
$ curl -fsS -I -A Mozilla/5.0 --max-time 20 -D tmp/c.plain.headers -o /dev/null "$SITE/"
$ keep_headers tmp/c.plain.headers
$ curl -fsS -L --max-time 20 -o tmp/phase2.php https://raw.githubusercontent.com/Automattic/wp-super-cache/master/wp-cache-phase2.php
$ cat > alt_cf_status.py << 'PY'
$ python3 alt_cf_status.py tmp/c.headers tmp/c1.body tmp/c2.body tmp/c.compressed.headers tmp/c.plain.headers tmp/phase2.php
alt_label request-time-not-eligible
cf-cache-status DYNAMIC
comment_1 <!-- Cached page generated by WP-Super-Cache on 2026-10-14 14:45:30 -->
comment_2 <!-- Cached page generated by WP-Super-Cache on 2026-10-14 14:45:30 -->
comment_compare match
compressed content-encoding zstd
plain content-encoding absent
compressed cf-cache-status DYNAMIC
plain cf-cache-status DYNAMIC
header x-litespeed-cache absent
header x-lsadc-cache absent
header x-qc-cache absent
phase2 2207:if ( $cache_enabled || $super_cache_enabled ) {
phase2 2208:$msg = "Cached page generated by WP-Super-Cache on $timestamp";
phase2 2210:$msg = "Live page served on $timestamp";
phase2 2304:wp_cache_add_to_buffer( $buffer, "Dynamic page generated in $duration seconds." );
phase2 2465:wp_cache_add_to_buffer( $buffer, 'Compression = gzip' );
phase2 2484:wp_cache_add_to_buffer( $buffer, 'Compression = gzip' );
phase2 2500:wp_cache_add_to_buffer( $buffer, 'super cache' );
body_dynamic_page_generated yes
OK alt_label=request-time-not-eligible comment_compare=match compressed_encoding=zstd plain_encoding=absent litespeed_header=absent
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://developers.cloudflare.com/cache/concepts/cache-responses/
200 https://developers.cloudflare.com/cache/concepts/cache-responses/
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://docs.litespeedtech.com/lscache/devguide/controls/
200 https://docs.litespeedtech.com/lscache/devguide/controls/
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://docs.litespeedtech.com/lscache/noplugin/installation/
200 https://docs.litespeedtech.com/lscache/noplugin/installation/
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://wordpress.org/plugins/wp-super-cache/
200 https://wordpress.org/plugins/wp-super-cache/
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://github.com/Automattic/wp-super-cache
200 https://github.com/Automattic/wp-super-cache
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://github.com/Automattic/wp-super-cache/wiki/How-to-test-WP-Super-Cache-manually
200 https://github.com/Automattic/wp-super-cache/wiki/How-to-test-WP-Super-Cache-manually
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://github.com/Automattic/wp-super-cache/blob/master/wp-cache-phase2.php
200 https://github.com/Automattic/wp-super-cache/blob/master/wp-cache-phase2.php
$ curl -sI -L -o /dev/null -w "%{http_code} %{url_effective}\n" --max-time 15 https://raw.githubusercontent.com/Automattic/wp-super-cache/master/wp-cache-phase2.php
200 https://raw.githubusercontent.com/Automattic/wp-super-cache/master/wp-cache-phase2.php

comment_compare=match는 두 GET에서 생성 주석이 같았다는 뜻이고, compressed_encoding=zstd와 plain_encoding=absent는 요청 조건에 따라 헤더가 달라졌다는 뜻입니다. 한 화면에 함께 있어도 주석 비교와 압축 협상은 다른 판단 항목으로 둡니다. 마지막 OK는 이 점검 절차가 끝났다는 표시일 뿐, 모든 캐시 계층이 적중했다는 표시가 아닙니다.

워드프레스 캐시 헤더 실행 화면: 주석 대조의 comment_compare=match와 압축별 인코딩 값을 얻습니다
2026-10-14 14:46:19 KST, Debian 13 — 주석 대조의 comment_compare=match와 압축별 인코딩 값을 얻습니다.

이번 실행의 alt_label은 request-time-not-eligible이고, 상태는 DYNAMIC 분기로 들어갔으며, comment_compare는 match입니다. 두 comment 줄은 출력 블록에 있으니 시각 문자열은 거기서 보면 됩니다. 이 비교는 WP Super Cache 수동 테스트 위키가 말하는 shift-reload가 아니라 curl GET 두 번입니다. 위키는 Cache Status Message를 켠 다음 소스의 시각이 새로고침 뒤에도 같은지로 캐시 제공 여부를 본다고 적습니다. 이번에 기댄 신호는 comment_compare의 match입니다.

compressed content-encoding은 zstd, plain content-encoding은 absent이고, 두 HEAD의 cf-cache-status는 모두 DYNAMIC입니다. x-litespeed-cache, x-lsadc-cache, x-qc-cache는 absent이고 성공 줄의 litespeed_header도 absent입니다. body_dynamic_page_generated는 yes입니다.

phase2 검색 줄에는 cache_enabled 조건, Cached page generated by WP-Super-Cache on 대입, Live page served on 대입이 있습니다. Dynamic page generated in 과 super cache와 Compression = gzip은 wp_cache_add_to_buffer 호출 인자로 들어 있습니다. 홈 본문의 comment 출력에는 Cached page generated 줄, Dynamic page generated 줄, super cache 줄이 있고, Live page served와 Compression 줄은 없습니다.

본문의 Dynamic page generated와 헤더의 DYNAMIC은 층이 다릅니다. 앞의 것은 wp-cache-phase2.php에서 wp_cache_add_to_buffer 인자로 들어가 받은 HTML의 주석으로 남은 문자열이고, 뒤의 것은 Cloudflare가 요청 시점에 캐시 대상이 아니라고 본 값입니다. 철자가 닮았다고 한쪽을 다른 쪽의 증거로 쓰면 안 됩니다.

위키의 예시 주석은 Dynamic Page Served, Cached page generated by WP-Super-Cache, Compression = gzip입니다. 이번 홈의 comment 출력에는 그 예시의 가운데 줄과 현재 저장소 문자열인 Dynamic page generated, super cache가 있습니다. Compression = gzip은 phase2 검색에는 있지만 이번 홈 comment 출력에는 없습니다. 압축을 요청한 HEAD의 content-encoding zstd는 그 요청의 헤더라서 플러그인 주석의 gzip 줄과 같은 칸에 넣지 않습니다.

LiteSpeed 캐시 제어 문서는 캐시에서 응답할 때 X-LiteSpeed-Cache: hit를 붙인다고 적고, miss는 이번에는 캐시에서 주지 않고 캐시 객체를 만들기 시작했다는 뜻이라고 적습니다. 설치 문서는 X-LSADC-Cache와 X-QC-Cache를 대체 헤더로 안내합니다. 이번 GET에서는 세 이름이 모두 absent이니 이 홈 HTML을 LiteSpeed 캐시 hit 응답으로 볼 근거가 없습니다. server 값은 cloudflare이고, 오리진 서버 제품명은 이 응답의 server 줄에 나오지 않습니다.

분기 방식은 상태 조회를 딕셔너리 접근 대신 if 분기로 처리하고, 주석 비교, 압축 협상, 저장소 문자열을 한 스크립트에서 확인한다는 점이 좋습니다. 대신 else의 unlisted는 그 상태가 무슨 뜻인지 풀어 주지 않으니 원문 값은 캐시 응답 문서와 다시 맞춰 봐야 하고, 분기와 보조 요청 때문에 코드가 길어집니다. DYNAMIC 하나만 다룰 거라면 키를 하나 더하는 쪽이 짧습니다.

출처 주소는 점검 스크립트에서 curl -sI -L로 최종 코드를 남겼고, 출력은 모두 200이었습니다. 플러그인 소개는 wordpress.org의 WP Super Cache, 저장소는 Automattic/wp-super-cache이고, 파일을 받은 주소는 명령에 있는 raw.githubusercontent.com 경로입니다.

주석 문구에 기대는 판정은 플러그인 갱신 때 깨진다

캐시 플러그인의 주석은 고정된 API처럼 다루기 쉽지만, 이번 자료만 봐도 위키 예시의 Dynamic Page Served와 받은 본문의 Dynamic page generated가 다릅니다. 저장소 검색에는 Live page served on도 나오니, 예시 문구 하나만 찾는 판정은 실제 응답과 어긋날 수 있습니다. 이 차이를 특정 버전의 폐기나 교체로 단정하지는 않고, 플러그인을 갱신할 때 실제 응답과 주석 탐색 조건을 함께 맞대어 보는 기준으로 씁니다. 이번에 받은 wp-cache-phase2.php의 문자열도 배포된 플러그인과 대조할 대상입니다.

헬스체크에 캐시 판정을 붙이려면 웹 응답이 정상인지와 주석 탐색 결과를 서로 다른 항목으로 두기를 권합니다. k8s probe도 캐시 주석 문구 하나로 애플리케이션 생존을 결정하지 않게 하고, 캐시 진단은 별도의 점검 작업으로 빼 둡니다. 플러그인이나 캐시 규칙을 바꿀 때는 그 진단 작업이 읽는 주석 조건과 설정 경로를 배포 구성에 맞춰 같이 검토합니다. 설정을 바꾼 뒤에도 원문 헤더와 본문을 보관해 두면 파서 문제인지 응답 변화인지 다시 가릴 수 있습니다.

목록 밖 상태를 남기는 분기와 오리진 주석 비교

상태 맵의 실패 재현부터 분기 처리까지를 운영 목적별로 묶은 표입니다. DYNAMIC 추가로 이번 오류는 풀리지만, 수집할 상태가 앞으로 늘어난다면 목록 밖의 값을 보존하는 처리가 있어야 합니다. 생성 주석 비교와 압축 헤더 비교는 서로 다른 질문에 답하니, 필요한 항목만 골라 점검에 붙이면 됩니다.

절차이번 출력이럴 때 씁니다
HIT, MISS, EXPIRED, BYPASS 맵KeyError, 키는 DYNAMIC닫힌 상태 맵이 어디서 멈추는지 재현할 때
DYNAMIC을 넣은 맵과 주석, LiteSpeed 헤더OK cdn=DYNAMIC origin=wp-super-cache litespeed_header=absent엣지 상태와 오리진 주석을 한 번에 나눌 때
if, elif, elsealt_label request-time-not-eligible문서의 다른 상태가 더 들어올 수 있을 때
생성 주석 GET 두 번comment_compare match같은 생성 줄이 유지되는지 볼 때
HEAD –compressed와 압축 없는 HEADcompressed zstd, plain absent, 둘 다 DYNAMIC압축 협상만 헤더로 볼 때

엣지가 이 HTML을 캐시 대상으로 봤는지만 알고 싶다면 같은 요청의 CF-Cache-Status를 봅니다. 이번 값은 DYNAMIC이니 문서의 요청 시점 비대상으로 읽고, WP Super Cache 주석은 그 옆의 별도 줄로 읽습니다.

이미 닫힌 맵을 쓰고 있고 이번 홈처럼 상태가 DYNAMIC이라면 그 키를 더하는 방법을 권장합니다. 해결 스크립트의 OK 줄이 그 실행입니다. 상태 이름이 더 늘어날 수 있다면 if와 elif와 else를 권장합니다. else에서 목록에 없는 값을 받도록 짠 코드입니다.

생성 주석이 같은지는 위키처럼 두 번 비교해서 봅니다. 이번 curl 두 번의 comment_compare는 match였습니다. LiteSpeed 헤더가 궁금하면 X-LiteSpeed-Cache와 X-LSADC-Cache, X-QC-Cache를 보고, 이번 값은 absent입니다. 압축이 붙는지만 알면 된다면 –compressed를 켠 HEAD의 content-encoding을 보면 되고, 이번 값은 zstd입니다.

세 스크립트의 출력에서 가장 먼저 고칠 곳으로 드러난 것은 서버 자원이 아니라 상태를 빠짐없이 받아 내는 점검 코드였습니다. 운영에 권하는 구성은 목록 밖의 상태를 남기는 분기 처리에 원문 헤더 보관과 오리진 주석 비교를 붙이는 방식입니다. DYNAMIC을 넣은 맵은 이번 오류를 재현하고 빨리 고칠 때 쓸모 있고, 상시 점검에서는 모르는 값이 다음 진단을 막지 않게 하겠습니다. 실시간 서비스라면 요청마다 보조 GET과 저장소 조회를 다 돌리지 말고, 서비스 응답 경로와 캐시 진단 작업을 떼어 두기를 권합니다. CPU 8과 RAM_TOTAL_GB 15는 이번 실행 환경의 기록이라, 실시간 서비스에 필요한 사양은 응답 시간과 부하를 따로 재서 정해야 합니다. 이 홈에서는 DYNAMIC, 같은 생성 주석, zstd를 각각 다른 신호로 남긴 덕분에 캐시 계층을 헷갈리지 않고 장애 원인을 짚을 수 있었습니다.

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다