AWS Cognito — access token revoked after session invalidation
RCA: Api::V1::UsersController#destroy Latency (avg 4824ms)
Overview#
What Happened#
2026-05-26 08:15~08:21 UTC 사이에 cupixworks-api 서비스의 UsersController#destroy 엔드포인트에서 평균 4824ms, 최대 4902ms의 응답 지연이 5건 발생했다. 동일 시간대에 한 명의 관리자가 15명의 사용자를 일괄 삭제하는 작업을 수행했으며, 그 중 5건에서 AWS Cognito API 호출이 비정상적으로 느려지면서 전체 요청 시간이 급증했다.
Quick Facts#
| Field | Value |
|---|---|
| resource_name | Api::V1::UsersController#destroy |
| top_frame | app/repositories/user_repository.rb:141 |
| env | production, us-west-2 |
| avg_duration | 4824ms |
| max_duration | 4902ms |
| HTTP status | 204 (모든 요청 성공) |
Affected Teams#
| Team / Domain | Error Count | Impact |
|---|---|---|
| cupixworks-api (user management) | 5 | 관리자의 사용자 삭제 요청이 ~5초 지연. 기능적 실패는 없으나 UX 저하 |
Timeline#
- 08:05:14 UTC — 관리자(Mustapha Djaziri)가 사용자 일괄 삭제 시작 (15명)
- 08:15:33 UTC — 첫 번째 slow request 발생 (user 13489, 4703ms)
- 08:21:51 UTC — 마지막 slow request (user 41919, 4841ms)
- 08:21:56 UTC — 일괄 삭제 작업 완료, 이후 재발 없음
Error Log#
{
"resource_name": "Api::V1::UsersController#destroy",
"service": "cupixworks-api",
"occurrences": 5,
"avg_ms": 4824,
"max_ms": 4902,
"sample_trace_id": "97686102836197600"
}
Impact#
- Service:
cupixworks-api - 발생 횟수: 5
- 최초 발생: 2026-05-26T08:15:33.811Z
- 최근 발생: 2026-05-26T08:21:51.540Z
Root Cause Summary#
UsersController#destroy는 사용자 삭제 시 UserRepository#remove 내에서 동기적으로 AWS Cognito AdminUpdateUserAttributes API를 호출하여 이메일/이름을 익명화한다. 이 Cognito API 호출이 간헐적으로 4~5초가 소요되면서 전체 요청 응답 시간이 4.8초로 증가했다. DB 쿼리 시간은 40200ms로 정상이며, 지연의 거의 전부가 외부 Cognito API의 응답 대기 시간이다. 15건의 삭제 요청 중 5건에서만 발생한 것으로 보아 Cognito 서비스의 간헐적 지연(throttling 또는 네트워크 지연)이 원인이다.
Technical Analysis#
Code Path#
- Entry point:
app/controllers/api/v1/users_controller.rb:48 - Core logic:
app/repositories/user_repository.rb:141-152 - Cognito call:
lib/cupix/aws/cognito/user.rb:74-79
1단계: Controller → Repository 호출
def destroy
repository_instance.remove
render_api
end
2단계: UserRepository#remove — 삭제 로직 수행
def remove
raise ... unless Pundit.policy(current_user, @model).delete?
delete_user = DeletedUser.new(@model.attributes.except('id').slice(*DeletedUser.attribute_names).merge(user_id: @model.id, deleted_at: DateTime.now))
delete_user.save
@model.reset_cycle_state unless @model.cycle_state_created?
@model.deleted
@model.assign_attributes(email: Digest::SHA1.hexdigest("#{@model.id},#{@model.email}") + '@removed user', firstname: nil, lastname: nil, state: 'inactive')
@model.save
end
@model.save가 호출되면 email, firstname, lastname 변경이 감지되어 after_update :update_cognito_user 콜백이 트리거된다.
3단계: Cognito 동기 호출 (병목 지점)
after_update :update_cognito_user
def update_cognito_user
return unless saved_change_to_firstname? || saved_change_to_lastname?
Cupix::Aws::Cognito.update_user!(self) if Cupix::Aws::Cognito.user_exists?(email_before_last_save)
end
이 메서드는 user_exists?(Cognito list_users API) + update_user!(Cognito admin_update_user_attributes API) 두 번의 동기적 HTTP 호출을 수행한다. 정상 시 500ms이나, Cognito 응답이 느린 경우 45초까지 소요된다.
4단계: after_deleted 콜백 — 권한 cascading 삭제
after_deleted do |model|
model.cleanup_avatar! if model.avatar?
model.sales_info.destroy if model.sales_info.present?
model.groups.each { |group| group.users.destroy(model) } if model.groups.exists?
model.team_permissions.destroy if model.team_permissions.exists?
model.workspace_permissions.destroy if model.workspace_permissions.exists?
model.facility_permissions.destroy if model.facility_permissions.exists?
# ... 7개 추가 permission 타입
end
각 permission은 dependent: :destroy로 하위 레코드를 cascade하며, 각 레코드 삭제 시 Elasticsearch _delete_document HTTP 호출이 동기적으로 발생한다. 그러나 이 단계의 총 소요 시간은 DB time(40~200ms) 내에 포함되어 있어 주 병목은 아님.
Log Evidence#
Datadog 로그에서 확인한 요청별 duration 분석:
service:cupixworks-api @http.url_details.path:"/api/v1/users/*" @http.method:DELETE @duration:>3000000000
핵심 로그 (slow requests):
{"timestamp": "2026-05-26T08:15:40.264Z", "user_id": 13489, "duration_ms": 4703.60, "db_ms": 40.21, "host": "ip-10-1-19-190"}
{"timestamp": "2026-05-26T08:17:33.512Z", "user_id": 13498, "duration_ms": 4782.31, "db_ms": 61.88, "host": "ip-10-1-144-228"}
{"timestamp": "2026-05-26T08:17:47.900Z", "user_id": 13502, "duration_ms": 4901.10, "db_ms": 198.22, "host": "ip-10-1-80-134"}
{"timestamp": "2026-05-26T08:19:29.701Z", "user_id": 13488, "duration_ms": 4882.24, "db_ms": 69.63, "host": "ip-10-1-144-228"}
{"timestamp": "2026-05-26T08:21:56.904Z", "user_id": 41919, "duration_ms": 4841.45, "db_ms": 93.92, "host": "ip-10-1-19-190"}
Cognito 호출 증거 — 동일 request_id에서 correlate된 로그:
service:cupixworks-api "update_cognito_user" @usr.id:(13489 OR 13498 OR 13502 OR 13488 OR 41919)
{"timestamp": "2026-05-26T08:15:40.295Z", "message": "update_cognito_user", "user_id": 13489, "changes": {"email": ["mlevesque@stl.laval.qc.ca", "hash@removed user"]}}
{"timestamp": "2026-05-26T08:17:33.557Z", "message": "update_cognito_user", "user_id": 13498, "changes": {"email": ["ddesmarais@stl.laval.qc.ca", "hash@removed user"]}}
{"timestamp": "2026-05-26T08:19:29.710Z", "message": "update_cognito_user", "user_id": 13488, "changes": {"email": ["mlebeau@stl.laval.qc.ca", "hash@removed user"]}}
정상 속도 요청과의 비교:
{"timestamp": "2026-05-26T08:14:53.652Z", "user_id": 13496, "duration_ms": 467.15, "db_ms": 66.49}
{"timestamp": "2026-05-26T08:15:19.308Z", "user_id": 13493, "duration_ms": 396.87, "db_ms": 38.22}
{"timestamp": "2026-05-26T08:16:27.744Z", "user_id": 13485, "duration_ms": 518.56, "db_ms": 52.59}
DB time은 slow/fast 요청 모두 유사(40~200ms)하지만, total duration이 10배 차이나므로 병목은 DB 외부(Cognito API)에 존재.
Hypotheses Considered#
| # | Hypothesis | Evidence for | Evidence against | Verdict |
|---|---|---|---|---|
| H1 | Cognito API 동기 호출의 간헐적 지연 | duration - db_time = ~4.7s, Cognito 로그 timestamp와 request 완료 시간이 일치, slow/fast 요청의 DB time 동일 | — | Confirmed |
| H2 | 대량 permission cascading 삭제로 인한 DB 지연 | 코드에서 10+ permission 타입 순차 삭제 + ES 호출 확인 | DB time이 40~200ms로 낮음, slow/fast 요청 간 DB time 차이 미미 | Rejected |
| H3 | N+1 쿼리 문제 | groups.each로 순차 삭제 패턴 존재 | 총 DB time이 여전히 200ms 미만으로 전체 지연 설명 불가 | Rejected |
| H4 | S3 avatar cleanup 지연 | 같은 시간대 me-south-1에서 S3 timeout 확인 | 이 요청들은 us-west-2에서 발생, S3 timeout은 별도 리전의 worker 이슈 | Rejected |
Fix Recommendation#
즉시 조치 (Critical)#
- 파일:
lib/cupix/aws/cognito/user.rb:74-79 - Cognito
update_cognito_user호출을 비동기로 전환.after_update콜백에서 직접 Cognito API를 호출하는 대신, Sidekiq worker로 위임하여 요청 응답 시간에서 분리. - Cognito API 호출에 timeout 설정 추가 (현재 무제한으로 보임). 최대 3초 timeout을 설정하여 최악의 경우에도 요청이 과도하게 지연되지 않도록 방어.
단기 개선 (1주 이내)#
UserRepository#remove전체 삭제 흐름을 background job으로 이동. Controller에서는 soft-delete만 수행하고 즉시 204 반환, 실제 cleanup(Cognito 익명화, permission 삭제, ES 업데이트)은UserCleanupWorker에서 비동기 처리.- Permission 삭제 시
destroy_all대신delete_all+ bulk ES 삭제를 사용하여 개별 콜백 호출 제거.
장기 개선 (재발 방지)#
- 사용자 삭제 프로세스를 이벤트 기반 아키텍처로 리팩터링.
UserDeletedEvent를 발행하고, 각 관심사(Cognito 익명화, permission 정리, ES 인덱스 정리, Segment 통지)가 독립적인 subscriber로 비동기 처리. - Bulk user deletion API 제공 — 관리자가 다수 사용자를 삭제할 때 단일 batch job으로 처리하여 개별 요청 지연 없이 작업 완료.
Monitoring#
UsersController#destroyp95 duration 알림 추가 (threshold: 2000ms)- Cognito API 호출 duration 전용 메트릭 추가
service:cupixworks-api resource_name:"Api::V1::UsersController#destroy" @duration:>3000000000
service:cupixworks-api "update_cognito_user" @duration:>2000000000
Risk Assessment#
- Risk level: low — 기능적 실패 없이 응답 지연만 발생. 모든 삭제 요청이 정상 완료(204).
- 예상 복잡도: standard — Cognito 호출을 worker로 이동하는 것은 기존 Sidekiq 인프라 활용 가능. Permission 삭제 최적화는 callback 의존성 검토 필요.