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 편에서 다룬다.