Telegram 봇 polling 409 충돌 디버깅 — 누가 몰래 getUpdates 를 돌리고 있었다

Telegram 봇 답글 처리 스크립트가 가끔 메시지를 통째로 놓친다. 로그를 보니 익숙한 에러가 보였다.

Conflict: terminated by other getUpdates request;
  make sure that only one bot instance is running

Telegram Bot API 의 409. 같은 봇 토큰으로 getUpdates 폴링을 두 군데 이상에서 동시에 하면 어느 쪽이든 이 에러를 받는다. 분명 폴링은 한 곳에서만 한다고 생각했는데, 누가 몰래 돌리고 있었던 거였다.

1. 알려진 소비자 점검 — 한 곳뿐

이 봇 토큰을 쓰는 곳을 머릿속으로 정리해봤다.

  • stock_report_reply.sh — 답글 처리 스크립트. polling.
  • n8n 워크플로 — 아직 비활성.
  • 그 외 — 없는 줄 알았다.

코드 그렙으로 더 확인해도 토큰을 직접 쓰는 코드는 한 군데뿐이었다. 그런데도 409 가 뜬다는 건 토큰을 암시적으로 얻어가는 누군가가 있다는 뜻.

2. 진범 — Environment=TELEGRAM_BOT_TOKEN=… 가 들어있는 systemd 유닛

서버에는 자동화 도구로 쓰는 게이트웨이 데몬이 systemd user unit 으로 떠 있다. 그 유닛 파일을 열어봤다.

# ~/.config/systemd/user/openclaw-gateway.service
  [Service]
  Environment=TELEGRAM_BOT_TOKEN=<봇 토큰>
  ExecStart=/home/ubuntu/.nvm/.../node openclaw/dist/index.js gateway --port 18789

이 게이트웨이는 환경에 TELEGRAM_BOT_TOKEN 이 있으면 자동으로 Telegram 채널을 활성화해서 백그라운드 폴링을 시작하는 설계였다. 도구의 CLI 로 channels list 를 쳤더니:

telegram: not configured, token=none

“설정 안 됐다, 토큰 없다” 라고 말하지만, 실제로는 systemd 가 주입한 env 를 픽업해 폴링 중이었다. CLI 의 표시와 실제 동작이 다른 거였다.

3. 왜 그 봇이 답글까지 빨아들였나

폴링은 가로채기 게임이다. 두 클라이언트가 같은 봇 토큰으로 getUpdates 를 부르면, 한쪽이 update 를 가져가는 순간 그 update 의 offset 이 advance 되고, 다른 쪽은 영영 못 본다. 게이트웨이는 폴링으로 받은 메시지 중 자기 “pairing” 컨텍스트가 아닌 건 드랍하지만, 드랍한 update 도 offset 은 advance 시킨다. 답글 스크립트가 정작 받아야 할 메시지가 그렇게 사라진 거였다.

4. 해결 — config 설정으로는 안 막힌다, env 자체를 끊어야

가장 먼저 시도한 건 도구의 설정값으로 비활성화하는 거였다.

openclaw config set channels.telegram.enabled false
  systemctl --user restart openclaw-gateway.service

효과 없음. 다음 부팅 때 env 가 살아있으면 도구가 그걸 보고 다시 켜버린다. 진짜 답은 systemd 유닛에서 env 줄을 빼는 것.

 [Service]
  -Environment=TELEGRAM_BOT_TOKEN=<봇 토큰>
   ExecStart=/home/ubuntu/.nvm/.../node openclaw/dist/index.js gateway --port 18789
systemctl --user daemon-reload
  systemctl --user restart openclaw-gateway.service

이후 409 가 사라졌고, 답글 스크립트가 모든 메시지를 정상적으로 받기 시작했다.

5. 잃은 것 — 게이트웨이로 보내던 Telegram 전송도 죽었다

부작용이 하나 있다. openclaw message send --channel telegram 같은 명령으로 메시지를 보내던 흐름도 같이 막혔다. 받기뿐 아니라 보내기도 같이 죽은 셈. 보내기는 무겁지도 않으니 직접 Telegram REST 로 갈아탔다.

curl -s "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
    -d "chat_id=${CHAT_ID}" \
    -d "text=hello" \
    -d "parse_mode=HTML"

Discord 쪽은 webhook 방식이라 게이트웨이의 폴링과 무관했고, 그 경로는 그대로 둘 수 있었다.

교훈

  • CLI 의 “비활성화” 표시를 믿지 말 것. 환경변수가 살아 있으면 도구가 자동으로 활성화하는 설계가
    흔하다.
  • systemd Environment= 는 디버깅 시 가장 먼저 의심할 곳. systemctl show
    <unit> --property=Environment
    로 즉시 확인.
  • Telegram getUpdates 는 단일 소비자 원칙. 한 봇 토큰의 폴링은 정확히 한 군데에서. 두 데몬을
    살리고 싶다면 한쪽은 webhook 으로 가야 한다.
  • 같은 봇이 n8n webhook 모드로 옮겨가면 setWebhook 이 등록되어 모든 폴링 시도가 영구 409. 폴링 → webhook 전환 시 폴링 측을 먼저 끄는 게 깔끔하다.

이 디버깅의 직접적인 동기는 Telegram + YouTube 요약 워크플로 의 webhook 등록이었고, n8n 인스턴스의 HTTPS 는 Nginx + Let’s Encrypt 편에서 다룬다.

참고 자료

댓글 달기

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

위로 스크롤