ES /docs

InvitationOperation synchronous Cognito calls blocking request thread

RCA: Api::V1::TeamsController#invitation Latency

Overview#

What Happened#

2026-05-26 03:26~08:27 UTC 사이에 cupixworks-api의 TeamsController#invitation 엔드포인트에서 평균 5172ms, 최대 9275ms의 비정상적 latency가 5개 리전(ca-central-1, ap-southeast-1, ap-northeast-1, ap-southeast-2, us-west-2)에서 8건 발생했다. 원인은 초대 플로우에서 새 사용자 생성 시 AWS Cognito API 호출, Segment Analytics identify, Kinesis event publish가 HTTP 요청 라이프사이클 내에서 동기적으로 순차 실행되기 때문이다.

Quick Facts#

Field Value
resource_name Api::V1::TeamsController#invitation
top_frame app/operations/invitation_operation.rb:61-94
runtime Ruby on Rails (cupixworks-api)
env production (multi-region)

Timeline#

  1. 2026-05-26T03:26:44Z — 최초 latency 감지 (ap-southeast-2, 1541ms)
  2. 2026-05-26T03:30~03:33Zliam.hong@cupix.com이 5개 리전에 동시 초대 수행 (1282~2740ms)
  3. 2026-05-26T08:27:57Z — 최대 latency 발생 (us-west-2, 9274ms, 2명 동시 초대)
  4. 2026-05-27 — RCA 분석 완료

Error Log#

Datadog Logs

json
{
  "resource_name": "Api::V1::TeamsController#invitation",
  "service": "cupixworks-api",
  "occurrences": 6,
  "avg_ms": 1837,
  "max_ms": 2742,
  "sample_trace_id": "1536084496537550476"
}

Impact#

  • Service: cupixworks-api
  • 발생 횟수: 8
  • 최초 발생: 2026-05-26T03:26:44.534Z
  • 최근 발생: 2026-05-26T08:27:57.758Z
  • 영향 범위: 모든 프로덕션 리전의 팀 초대 기능. 사용자가 여러 명을 초대할 때 응답 시간이 초대 인원 수에 비례하여 증가 (1명당 ~2-3초).

Root Cause Summary#

InvitationOperation.invite_people가 초대 이메일 목록을 순차 루프로 처리하면서, 각 신규 사용자 생성 시 after_create 콜백에서 AWS Cognito API 호출 (create_cognito_user)과 Segment Analytics identify (segment_user_event), 그리고 Kinesis put_records (EventService.publish_event)가 모두 HTTP 요청 내에서 동기 실행된다. 2명을 초대하면 Cognito 왕복 2회(각 ~2초) + Segment 2회 + Kinesis 2회가 순차로 누적되어 9초 이상 소요된다.

Technical Analysis#

Code Path#

  • Entry point: app/controllers/api/v1/teams_controller.rb:40-44
app/controllers/api/v1/teams_controller.rb:40-44ruby
def invitation
  InvitationOperation.invite_people(params, current_user, @current_team)
  render_json 200, { user_emails: params[:user_emails] }
end
  • 핵심 루프: app/operations/invitation_operation.rb:61-94
app/operations/invitation_operation.rb:61-94ruby
invitees = emails.map do |email|
  begin
    invitee = User.find_by(team: current_team, email: email)

    if invitee.present?
      # ... update existing user state ...
      invitee.save!
    else
      user_factory = UserFactory.new(current_user: current_user, current_team: current_team, skip_permission_check: params[:skip_permission_check])
      invitee = user_factory.create!({
        team: current_team,
        email: email
      })
    end
  end

  send_invitation_mail({ user_email: email, redirect_url: redirect_url }, current_user, current_team, invitee)
  invitee
end

각 이메일에 대해 순차적으로 User.find_byuser_factory.create!send_invitation_mail 수행. 신규 사용자일 경우 create!에서 User.new + save!after_create 콜백 체인 발동.

  • Cognito 동기 호출: lib/cupix/aws/cognito/user.rb:10,54-72
lib/cupix/aws/cognito/user.rb:10,54-72ruby
after_create :create_cognito_user, unless: :skip_cognito_user_creation

def create_cognito_user
  if Cupix::Aws::Cognito.user_exists?(self.email)
    cognito_user = Cupix::Aws::Cognito.get_user_by_email(self.email)
    self.update!(cognito_user_id: cognito_user.attributes.find { |attr| attr.name == 'sub' }.value)
  else
    cognito_user_creation_response = Cupix::Aws::Cognito.create_user!(self)
    self.update!(cognito_user_id: cognito_user_creation_response.user.attributes.find { |attr| attr.name == 'sub' }.value)
  end
end

after_create 콜백으로 등록된 이 메서드는 AWS Cognito User Pool에 HTTP 요청을 보내 사용자를 생성한다. 리전 간 latency에 따라 1회 호출당 1~3초 소요.

  • Segment identify 동기 호출: app/models/concerns/eventable/user.rb:8,12-29
app/models/concerns/eventable/user.rb:8,12-29ruby
after_create :segment_user_event, if: :send_segment_event?

def segment_user_event
  ::Analytics.identify(
    user_id: crn,
    traits: {
      email: email,
      firstname: firstname,
      lastname: lastname,
      teamId: team_id,
      # ...
    }
  )
end
  • Kinesis event publish 동기 호출: lib/cupix/event_service.rb:21-54
lib/cupix/event_service.rb:43ruby
response = Cupix::Aws::Kinesis.put_records!({ stream_name: stream_name, records: records })

UserFactory.create!에서 track_team_event! 플래그를 설정하여 after_create 시 Kinesis에 이벤트를 동기 publish한다.

  • Failure point: 단일 failure point은 아님. 누적 latency 문제로, N명 초대 시 N × (Cognito + Segment + Kinesis + DB) 시간이 동기적으로 합산됨.

Log Evidence#

Datadog 검색 쿼리:

text
service:cupixworks-api TeamsController invitation
Time: 2026-05-26T02:26:00Z to 2026-05-26T09:00:00Z

가장 느린 요청 (9274ms, request_id: 7e09729c-0366-4a9c-857e-9d4a75d04554):

text
08:28:05.564Z - Cognito.create_cognito_user: "Creating user in Cognito: osaulnier-boileau@stl.laval.qc.ca"
08:28:05.564Z - Cognito.create_cognito_user: "User created in Cognito: osaulnier-boileau@stl.laval.qc.ca"
08:28:05.564Z - User.segment_user_event: "Segment event sent for User 49610"
08:28:05.564Z - Cupix::EventService.publish_event: "Published event - failed_record_count: 0 / 1"
08:28:07.565Z - Cognito.create_cognito_user: "Creating user in Cognito: mdavid@stl.laval.qc.ca"
08:28:07.565Z - Cognito.create_cognito_user: "User created in Cognito: mdavid@stl.laval.qc.ca"
08:28:07.565Z - User.segment_user_event: "Segment event sent for User 49611"
08:28:07.565Z - Cupix::EventService.publish_event: "Published event"
08:28:07.518Z - [200] POST /api/v1/teams/invitation, duration: 9274.56ms, db: 2119.73ms

2명 초대 시 Cognito 생성이 순차로 2회 실행: 첫 번째 사용자 생성 후 ~2초 뒤 두 번째 사용자 생성 시작. DB 시간(2119ms)을 제외한 ~7155ms가 외부 API 호출(Cognito + Segment + Kinesis)에 소비됨.

1명 초대 시 latency 비교 (같은 시간대):

text
03:32:10.230Z - ap-northeast-1, 1명 초대, duration: 2740ms, db: 37ms
03:33:00.776Z - ca-central-1, 1명 초대, duration: 2010ms, db: 29ms
03:32:45.915Z - ap-southeast-1, 1명 초대, duration: 1752ms, db: 41ms

1명 초대 시에도 ~1700-2740ms 소요 — Cognito API 단일 호출의 cross-region latency가 주요 원인.

Hypotheses Considered#

# Hypothesis Evidence for Evidence against Verdict
H1 동기적 Cognito API 호출이 주 latency 원인 로그에서 create_cognito_user 시작→완료 간 ~2초 확인, 2명 초대 시 순차 실행으로 ~4초 Confirmed
H2 N+1 DB 쿼리로 인한 latency 루프 내 find_by + save! per email (N+1 패턴) DB 시간은 최대 2119ms이나 전체 9274ms 중 23%에 불과. 1명 초대 시 DB 29-41ms로 미미 Rejected (부분 기여)
H3 이메일 발송(SendGrid)이 동기 실행 TeamInvitationMailerWorker.perform_async 사용 확인 (Sidekiq 비동기), Datadog에서 worker 로그 미발견 Rejected
H4 Segment/Kinesis 동기 호출 기여 로그 타임라인에서 Segment identify + EventService publish가 요청 내 실행 확인 개별 호출당 latency는 Cognito 대비 작음 (수백ms 수준) Confirmed (보조 원인)

Fix Recommendation#

즉시 조치 (Critical)#

  • lib/cupix/aws/cognito/user.rb:10after_create :create_cognito_user 콜백을 비동기 Sidekiq worker로 전환. 초대 플로우에서 cognito_user_id가 즉시 필요하지 않으므로 (invitation token 기반 인증), background job으로 이동 가능.
  • app/operations/invitation_operation.rb:61-94 — 루프 내 순차 처리 대신 bulk 사용자 생성 후 Cognito 호출을 별도 worker에서 일괄 처리하도록 분리.

단기 개선 (1주 이내)#

  • InvitationOperation.invite_people에서 emails.map 루프를 ActiveRecord::Base.transaction 블록 내에서 사용자만 먼저 생성하고, Cognito 등록은 별도 CognitoUserCreationWorker.perform_bulk 호출로 분리.
  • app/models/concerns/eventable/user.rb:8segment_user_eventafter_commit + async로 변경하거나, invitation 컨텍스트에서는 skip하도록 조건 추가.

장기 개선 (재발 방지)#

  • 초대 API를 비동기 패턴으로 재설계: API는 즉시 202 Accepted 반환 후, 실제 사용자 생성/Cognito 등록/이벤트 발행은 background pipeline에서 처리. 클라이언트에 WebSocket/polling으로 완료 통지.
  • Cognito 호출에 circuit breaker 패턴 적용 (latency spike 시 graceful degradation).

Monitoring#

  • 추가할 메트릭: TeamsController#invitation p95/p99 duration 알림 (threshold: 3000ms)
  • Cognito API 호출 latency 별도 tracking
text
service:cupixworks-api resource_name:"Api::V1::TeamsController#invitation" @duration:>3000ms env:production

Risk Assessment#

  • Risk level: medium
  • 예상 복잡도: standard — Cognito 콜백을 async worker로 이동하는 것은 기존 skip_cognito_user_creation! 메커니즘이 이미 존재하므로 패턴이 확립되어 있음. 다만, cognito_user_id가 후속 로직에서 참조되는 경우 의존성 확인 필요.