Telegram Bot API rate-limits each bot to approximately 1-2 concurrent uploads. When multiple upload chunks hit the same bot simultaneously, Telegram returns HTTP 429 (Too Many Requests), causing delays of 30-60 seconds per retry. Under Docker push load, these cumulative delays trigger Gitea client timeouts and `500 Internal Server Error`.
> Catatan (2026-08-02): Produksi sekarang port 4000, deploy Nix+systemd di orangevps, Caddy reverse proxy upload.asepharyana.my.id, DB via pgbouncer pool imrnes 100.121.180.82:6432. Docker/Traefik/Gitea-CI legacy.
The current architecture uses a **global PQueue** with `concurrency=N` where each task picks a bot via round-robin (`claimBotIndex()`). This means two concurrent tasks can both land on the same bot index (after wrap-around), causing 429 collisions.
## Solution: Per-Bot Queue
Each bot has its own PQueue with `concurrency=1`. Uploads are assigned to the bot with the fewest pending tasks. If a bot rate-limits, the upload moves to the next available bot.
│ │ └─ Return bot with smallest queue.pending count
│ │
│ ├─ attemptedIndexes.add(selectedBot)
│ │
│ ├─ result = await bots[selectedBot].queue.add(() =>
│ │ executeTelegramCall(bot, fileChunk, fileName)
│ │ )
│ │ │
│ │ ├─ ✅ Success → return ForwardResult
│ │ │
│ │ └─ ❌ Error
│ │ ├─ 429 → markRateLimited(bot, retryAfter)
│ │ │ → continue to next bot in retry loop
│ │ ├─ Transient (timeout, 5xx) → continue
│ │ └─ Non-transient → throw (propagate up)
│ │
│ └─ Attempt counter exhausted → throw lastError
│
└─ Sorted part tracking (for chunked uploads)
```
### Key Design Decisions
1.**Concurrency=1 per bot**: Guarantees no two Telegram API calls compete for the same bot token. With 6 bots, effective concurrency = 6.
2.**Least-loaded assignment**: `selectBot()` picks the bot with the fewest queued + pending tasks. This naturally load-balances even when some bots are slower.
3.**Rate-limit isolation**: When bot A hits 429, only bot A's queue is paused. Other 5 bots continue serving uploads uninterrupted.
4.**Per-bot rate-limit timer**: `rateLimitedUntil` prevents re-selecting a recently-429'd bot until its cooldown expires.
5.**No global PQueue**: The old `upload-queue.ts` is removed. Each bot owns its queue, eliminating the global backpressure problem.