원문 캡처 · blog.pypi.org
원문 캡처 · blog.pypi.org

2026년 8월 약 2주 동안 일부 PyPI 사용자가 files.pythonhosted.org에서 파일을 내려받을 때 간헐적으로 502·503 오류를 겪었다. Fastly 캐시 노드의 설정 문제와 PyPI의 Fastly 설정에 있던 별도 버그가 각각 확인됐으며, 두 문제는 8월 28일까지 해결됐다.

오류 원인과 파일 제공 방식

PyPI는 files.pythonhosted.org를 Fastly CDN으로 제공한다. 패키지 파일은 먼저 Amazon S3에 저장되고, 백그라운드 작업이 이를 Backblaze B2로 복사한다. Fastly는 우선 B2에서 파일을 가져오고, B2가 응답하지 않거나 예상과 다른 응답을 하면 S3를 대체 경로로 이용한다. B2에서 Fastly로 파일을 전송하는 데에는 저장 비용 외 별도 비용이 들지 않는 계약이 있다(Fastly와 Backblaze B2의 데이터 전송 안내). S3에는 저장 비용과 검색 비용의 균형을 위한 Glacier Instant Retrieval 저장 클래스가 적용됐다.

파일이 캐시에 저장된 뒤에는 원본을 다시 확인하지 않는다. 파일은 바뀌지 않는다는 전제 아래 Cache-Control 헤더(설명)가 max-age=365000000, immutable, public으로 설정돼 있어 캐시 기간은 약 11.5년이다. 패키지 파일 요청이 아닌 예측 가능한 URL(문서)과 일부 이전 리디렉션은 Conveyor라는 세 번째 백엔드가 처리한다.

\nflowchart TD\n Client([Client request]) --> Edge{Fastly edge}\n Edge -->|package file| B2[(B2: egress-free cache)]\n Edge -->|everything else| Conveyor[Conveyor]\n B2 -->|200 or 206| Response([Response to client])\n B2 -.->|404, timeout, or 5xx| Archive[(S3: origin, fallback)]\n Archive --> Response\n Conveyor --> Response\n```

보고서에 따르면, 기존 설정에는 B2가 오류 상태를 반환할 때만 S3 대체 경로를 이용하고 연결 시간 초과나 연결 거부, TLS 오류처럼 응답 자체가 없을 때는 대체 경로를 건너뛰는 버그가 있었다. 이 경우 Fastly가 503을 만들어 반환했다. 8월 18일 반영된 [infra#237](https://github.com/pypi/infra/pull/237)이 응답이 없을 때도 S3로 전환하도록 수정했다.

또 다른 원인은 일부 범위 요청(Range request)을 나눠 캐시하는 기능이었다. 이 기능은 파일 전체를 캐시에 가져오지 않고 일부만 전달하도록 돕지만, 접미 범위 요청(`bytes=-1024`)이나 범위 시작점이 파일 끝을 넘는 요청을 처리하지 못해 501 응답을 만들었다. 일부 설치 프로그램은 휠 파일 전체를 내려받지 않고 메타데이터를 읽기 위해 이런 접미 범위를 사용한다. PyPI는 [infra#241](https://github.com/pypi/infra/pull/241)과 [infra#243](https://github.com/pypi/infra/pull/243)을 통해 접미 범위와 다중 범위 요청을 분할 캐시 대상에서 제외했다. Fastly에는 이런 범위 요청 처리에 관한 지원 티켓도 접수했다.

URL 정규화 순서에도 문제가 있었다. 쿼리 문자열이 붙은 `.metadata` 요청은 URL 정규화 전에 예외 여부를 확인해 분할 캐시 제외 규칙에 걸리지 않았다. 그 결과 잘못된 501 응답이 캐시에 저장돼 같은 파일을 요청한 후속 사용자에게도 전달될 수 있었다. [infra#245](https://github.com/pypi/infra/pull/245)은 URL을 먼저 정규화한 뒤 예외를 확인하도록 바꿨다.

## 장애 경과와 복구

Fastly의 보고에 따르면 문제는 8월 15일 시애틀 지역의 한 캐시 노드에서 시작됐다. 8월 19일 PyPI 지원 게시물 [#11925](https://github.com/pypi/support/issues/11925)은 문제를 `cache-pae2080020` 노드로 좁혔다. 해당 노드로 라우팅된 요청은 19시간 넘게 502를 반환했으며, 실패 응답의 `x-served-by` 헤더로 확인됐다. Fastly 네트워크 운영팀은 영향을 받은 지점으로 트래픽 일부를 보내던 라우팅 재정의를 제거했고, 8월 20일에는 해당 지점의 복구와 오류율 정상화를 확인했다.

지원 게시물 [#11876](https://github.com/pypi/support/issues/11876), [#11895](https://github.com/pypi/support/issues/11895), [#11897](https://github.com/pypi/support/issues/11897), [#11908](https://github.com/pypi/support/issues/11908)에는 오류 신고와 재현 정보가 올라왔다. 특히 #11908은 6시간 동안 서로 관련 없는 32개 패키지에서 502 오류 88건을 기록했고, PEP 658 메타데이터([명세](https://peps.python.org/pep-0658/))를 읽을 때 쓰이는 작은 `.whl.metadata` 범위 요청도 실패했다고 보고했다.

8월 21일에는 논리적으로 잘못된 범위 요청을 보낸 단일 클라이언트로 오류가 급증했다. 8월 24일에는 제대로 동작하지 않는 병렬 다운로드 프로그램 두 개가 같은 유형의 오류를 하루 동안 41,315건 더 발생시켰다. PyPI는 8월 28일 Fastly가 내부 카나리 설정의 근본 문제를 수정하고 PyPI를 포함한 PSF 트래픽을 카나리 대상에서 제외했다고 밝혔다.

Fastly의 실시간 분석에서는 8월 15일부터 B2 오류가 늘어난 반면, S3 대체 경로의 오류는 0에 머물렀다. 대체 경로가 작동하지 않았기 때문이다. PyPI의 Datadog 지표에서는 장애 전부터 5xx 응답이 수천에서 수만 건에 이르는 로그 기준의 잡음이 있었고, 8월 21일에는 오류가 거의 100만 건까지 늘었다. [infra#241](https://github.com/pypi/infra/pull/241) 이후에는 오류량이 장애 전 기준선보다 2~3자릿수 낮은 수십에서 수백 건으로 줄었다. 보고서는 이전부터 배경 잡음으로 여겼던 오류 일부도 기존 설정 버그에서 비롯됐다고 설명했다.

## 카나리 설정과 사용자 대응

Fastly는 전체 배포에 앞서 일부 인프라에서 캐시·라우팅 소프트웨어를 시험하는 카나리 그룹을 운영해 왔다. PyPI 트래픽은 수년간 이 그룹에 포함돼 있었다. 이번에는 카나리 배포 중 일부 롤백으로 시애틀의 한 지점에서 캐시 설정만 되돌아가고, 앞단 라우팅 설정은 그대로 남았다. 이 불일치가 502 응답을 일으켰다. PyPI는 향후 트래픽에 대한 통제와 알림이 명확해지면 카나리 참여를 다시 검토할 수 있다고 밝혔다.

Python Software Foundation은 엔지니어링 인력 4명에 더해 인프라 엔지니어를 채용 중이며, 해당 직무는 PyPI도 담당할 예정이다. 보고서는 이를 통해 문제를 더 일찍 발견하고 사후 대응보다 예방에 나설 수 있을 것으로 기대한다고 설명했다.

GitHub Actions에서 같은 의존성을 반복 설치하는 경우 다운로드 캐시를 켜면 다음 실행에서 이미 캐시된 의존성은 PyPI에 요청하지 않는다. `setup-python`의 [pip·pipenv·poetry 캐시](https://github.com/actions/setup-python/blob/main/docs/advanced-usage.md#caching-packages)는 기본값이 꺼져 있고 `cache` 입력으로 활성화해야 한다. `setup-uv`의 [캐시](https://github.com/astral-sh/setup-uv/blob/e105c8fb1d7b13074b851babdaef4185243c6a07/docs/caching.md)는 대부분의 GitHub 호스팅 러너 이벤트에서 기본으로 켜져 있지만, 설정을 확인하는 것이 좋다고 보고서는 안내했다. 향후 파일 호스팅 문제가 발생하면 [pypi/support](https://github.com/pypi/support/issues)에 신고할 수 있으며, `x-served-by` 헤더와 발생 시각을 포함하면 대응에 도움이 된다.

## 부록: 원문 예시

```text
flowchart TD
 Client([Client request]) --> Edge{Fastly edge}
 Edge -->|package file| B2[(B2: egress-free cache)]
 Edge -->|everything else| Conveyor[Conveyor]
 B2 -->|200 or 206| Response([Response to client])
 B2 -.->|404, timeout, or 5xx| Archive[(S3: origin, fallback)]
 Archive --> Response
 Conveyor --> Response
text
flowchart LR
 subgraph Before["Before infra#241 and infra#243"]
 direction TB
 C1([Client: suffix or inverted range]) --> E1{Fastly edge}
 E1 --> S1[Segmented caching]
 S1 --> R1([501 Not Implemented])
 end
 subgraph After["After"]
 direction TB
 C2([Client: suffix or inverted range]) --> E2{Fastly edge}
 E2 -->|range shape not supported| N2[Segmented caching skipped]
 N2 --> R2([Normal range handling: 206 or 416])
 end
 Before ~~~ After
text
flowchart LR
 subgraph Before["Before infra#245"]
 direction TB
 C3([Client: <code>GET name.whl.metadata<b>?token=x</b></code>]) --> E3{Fastly edge}
 E3 -->|exemption checked before URL is normalized| S3[Segmented caching stays on]
 S3 --> B23[(B2)]
 B23 -->|501 for the 1MiB segment, cached at the edge| R3([Every later request: 501])
 end
 subgraph After2["After"]
 direction TB
 C4([Client: <code>GET name.whl.metadata<b>?token=x</b></code>]) --> E4{Fastly edge}
 E4 -->|URL normalized first| X4[Exemption matches: segmented caching off]
 X4 --> B24[(B2)]
 B24 --> R4([Normal response, cached correctly])
 end
 Before ~~~ After2