# Mỗi lần gọi API chỉ chờ 30 giây, người dùng vẫn phải đợi 90 giây

> Đồng hồ bị đặt lại sau mỗi bước, thêm retry thì lên tới 270 giây; cách sửa là một deadline duy nhất cho cả request.

Bản gốc: https://fdetimes.net/vi/bach-khoa/ngan-sach-timeout-chuoi-goi-api-agent/

Khách hàng gửi lên một ticket: trợ lý hỗ trợ khách hàng mà đội bạn vừa deploy thỉnh thoảng quay vòng chờ suốt 90 giây rồi mới báo lỗi. Bạn mở code ra xem thì thấy mọi lần gọi API đều có timeout 30 giây, đúng như checklist. Không có dòng nào sai, vậy mà người dùng vẫn phải ngồi chờ gấp ba con số trên giấy.

Lỗi này dễ lọt qua review chính vì từng dòng code đều trông đúng. FDE đưa agent vào production nên hiểu lỗi này trước tuần go-live, đừng đợi đến lúc nó hiện ra trong ticket của khách. Lý do khá đơn giản: timeout đang được đặt cho từng bước gọi, trong khi người dùng chỉ quan tâm cả request mất bao lâu.

Sửa lỗi này không cần thêm hạ tầng gì. Chỉ cần đổi cách nghĩ về thời gian, từ "mỗi lần gọi được chờ bao lâu" sang "cả request còn lại bao nhiêu giây".

## Ba lần 30 giây cộng lại thành 90 giây

Thử hình dung trợ lý kia xử lý câu hỏi "đơn hàng của tôi đâu?". Agent gọi CRM để tìm khách, gọi hệ thống đơn hàng để lấy trạng thái, rồi gọi LLM để viết câu trả lời. Mỗi lần gọi có timeout 30 giây riêng.

Một hôm hệ thống đơn hàng chậm. Tình huống tệ nhất là cả ba bước đều chạm trần, và 30 cộng 30 cộng 30 ra 90 giây.

Tianpan.co mô tả đúng cơ chế này trong một bài về chuỗi agent: agent tiếp tục lên lịch tool call kế tiếp với một timeout mới tinh, cứ như đồng hồ vừa được đặt lại.

Không ai trong chuỗi giữ tổng thời gian đã tiêu.

Thêm retry vào thì con số còn tệ hơn. Giả sử mỗi bước được thử tối đa 3 lần, khi đó trường hợp xấu nhất là 9 lần chờ 30 giây, tức 270 giây. Những lần chờ ấy cũng chẳng giúp được ai. Sách SRE của Google nói rõ: client không quan tâm server làm gì sau khi deadline đã qua.

## Deadline thuộc về request, không thuộc về từng bước

Sách SRE của Google cho rằng đặt deadline cho RPC thường là việc khôn ngoan, và đi xa hơn một bước: server không nên tự nghĩ ra deadline mới khi gọi xuống backend mà nên truyền tiếp deadline nhận được (deadline propagation).

Bài viết trên tianpan.co áp dụng nguyên tắc đó vào agent. Deadline được đặt một lần dưới dạng mốc thời gian tuyệt đối, và chính mốc đó được truyền cho mọi lần gọi phía sau chứ không phải một khoảng thời gian.

**Điểm mấu chốt:** Deadline thuộc về cả request, không thuộc về từng bước gọi.

Vì sao phải là mốc thời gian mà không phải khoảng thời gian? Một con số như "còn 20 giây" chỉ đúng vào lúc nó được tính ra. Thời gian request nằm trong hàng đợi hay trong vòng lặp suy nghĩ của agent không được trừ vào đó. Mốc "hết hạn lúc 10:00:30" thì luôn đúng, bước nào cũng tự trừ được.

Tài liệu gRPC cho thấy hai cách này cùng tồn tại trên thực tế. Có API nhận timeout (một khoảng thời gian), có API nhận deadline (một thời điểm), còn việc tự động truyền deadline từ RPC đến sang RPC đi thì chỉ một số bản cài đặt hỗ trợ. Bạn nên kiểm tra thư viện mình dùng, đừng mặc định là nó tự lo.

## Ví dụ: thêm ngân sách vào vòng lặp agent

Quay lại trợ lý hỗ trợ khách hàng. Giả sử khách hàng chấp nhận tối đa 30 giây cho cả câu trả lời. Đoạn Python dưới đây đủ để sửa lỗi trong một process:

```python
import random
import time

class Budget:
    def __init__(self, seconds):
        # Đặt deadline một lần, dùng đồng hồ monotonic để không bị lệch giờ hệ thống
        self.deadline = time.monotonic() + seconds

    def remaining(self):
        return self.deadline - time.monotonic()

class TransientError(Exception):
    pass

def call_tool(fn, budget, reserve=2.0, max_attempts=2):
    for attempt in range(max_attempts):
        left = budget.remaining() - reserve  # chừa thời gian để trả lời người dùng
        if left <= 0:
            raise TimeoutError("hết ngân sách, không gọi nữa")
        try:
            return fn(timeout=left)  # mỗi lần gọi chỉ nhận phần còn lại
        except TransientError:
            wait = min(0.2 * 2 ** attempt, left) * random.random()
            time.sleep(wait)  # backoff lũy thừa có jitter
    raise TimeoutError("hết số lần thử")

budget = Budget(30)
customer = call_tool(crm_lookup, budget)
order = call_tool(order_status, budget)
answer = call_tool(llm_summarize, budget)
```

Giờ hãy chạy lại kịch bản xấu. CRM trả về sau 3 giây, còn hệ thống đơn hàng treo. Lần gọi thứ hai chỉ được chờ khoảng 25 giây, tức 30 trừ 3 trừ 2 giây dự phòng.

Ví dụ này giả định `order_status` tự ném lỗi timeout của client khi hết 25 giây, và lỗi đó không phải `TransientError` nên `call_tool` không bắt nó mà để nó đi thẳng lên, không retry.

Kể cả nếu có vòng thử lại, ngân sách còn lại lúc ấy đã về 0 nên `call_tool` cũng sẽ dừng. Người dùng nhận lỗi ở khoảng giây thứ 28 chứ không phải giây thứ 90.

Với agent, bạn nên thêm một bước nữa: kiểm tra `budget.remaining()` trước khi để LLM lên kế hoạch cho tool call tiếp theo. Nếu thời gian còn lại ít hơn mức một bước thường tốn, trả câu trả lời dở dang kèm lời giải thích sẽ tốt hơn bắt đầu một việc chắc chắn không kịp xong.

## Retry ở mọi tầng sẽ nhân tải lên

Retry giúp người gọi có thêm cơ hội thành công, nhưng phía server phải tốn thêm thời gian cho việc đó. AWS Builders' Library gọi retry là "ích kỷ" vì lẽ này. Khi mỗi tầng retry độc lập, tải dồn xuống tầng dưới cùng sẽ nhân lên: ví dụ của AWS cho thấy tải lên database tăng 243 lần, nhiều khả năng không bao giờ hồi phục được.

Bạn có thể tự nhẩm với chính hệ thống của mình. Ba tầng, mỗi tầng thử 3 lần, nghĩa là một request của người dùng có thể thành 27 lần gọi vào dịch vụ đang chậm, đúng vào lúc nó yếu nhất.

Google SRE khuyên giới hạn số lần retry cho mỗi request và dùng backoff lũy thừa có yếu tố ngẫu nhiên. Đoạn code trên làm cả hai việc, và retry chỉ diễn ra trong phạm vi ngân sách.

## Áp dụng ở site khách hàng

Việc đầu tiên là vẽ chuỗi gọi ra giấy, ghi timeout và số lần retry của từng bước, rồi nhân ra thời gian tệ nhất. Con số này thường làm khách hàng bất ngờ, và nó là lý lẽ tốt nhất để được phép sửa.

Bước tiếp theo là hỏi khách hàng ngân sách cho cả request là bao nhiêu. AWS thừa nhận rằng chọn giá trị timeout thường là phần khó nhất, nên con số này cần đến từ trải nghiệm người dùng, không phải đoán.

Sau đó đặt deadline ở điểm vào duy nhất (API gateway hoặc handler đầu tiên) và truyền nó đi khắp nơi. Giữa các service thì dùng deadline của gRPC hoặc một header mang mốc thời gian, còn trong process thì dùng một đối tượng như `Budget`. Cuối cùng, mọi bước phải kiểm tra ngân sách trước khi bắt đầu một việc tốn kém.

## Những lỗi hay gặp

Lỗi phổ biến nhất là quên rằng mặc định có thể là vô hạn. Tài liệu gRPC ghi rõ gRPC không đặt deadline theo mặc định, nên một lần gọi có thể chờ mãi nếu client không đặt. Lỗi thứ hai là truyền "30 giây" xuống mọi tầng thay vì truyền phần còn lại, tức chính lỗi 90 giây ở đầu bài.

Lỗi thứ ba khó thấy hơn: tầng dưới vẫn chạy tiếp sau khi tầng trên đã bỏ cuộc. Một truy vấn database chạy xong ở giây thứ 40 cho request đã hết hạn ở giây thứ 30 là tài nguyên bỏ đi.

Lớp `Budget` ở trên chỉ chặn việc gọi thêm, nên bạn cần truyền tiếp `budget.remaining()` xuống thành timeout của chính truy vấn hoặc driver database để tầng dưới tự hủy khi request hết hạn.

Lỗi thứ tư là không chừa thời gian dự phòng, để deadline hết đúng lúc còn phải định dạng và gửi câu trả lời.

## Biến bài học thành một dòng trong CV

Kỹ năng này nên được ghi vào CV bằng con số cụ thể. Một dòng như "giảm thời gian chờ tệ nhất của agent hỗ trợ khách hàng từ 270 giây xuống dưới 30 giây bằng deadline propagation" có sức nặng hơn nhiều so với "có kinh nghiệm về reliability".

Nếu tin tuyển dụng FDE nhắc đến "production agents" hay "integration với hệ thống của khách hàng", đây là câu hỏi bạn nên chuẩn bị sẵn cho vòng thiết kế hệ thống. Hãy kể được phép nhân 90 giây và cách bạn đặt một deadline duy nhất cho cả request.

Một agent nhanh và một agent chậm có thể chạy cùng một model và cùng các tool. Thứ khác nhau giữa chúng là có ai trong chuỗi gọi theo dõi xem request còn lại bao nhiêu giây hay không.

**Thử ngay tuần này:**

- Chọn một agent hoặc service bạn đang deploy, liệt kê mọi lần gọi ra ngoài kèm timeout và số lần retry, rồi nhân ra thời gian chờ tệ nhất.
- Thêm một lớp Budget vào vòng lặp agent để nó dừng và trả kết quả dở dang khi phần thời gian còn lại không đủ cho tool call tiếp theo.
- Tìm mọi client gRPC trong codebase chưa đặt deadline và sửa chúng.

## Nguồn

- [Addressing Cascading Failures (Google SRE Book, Chapter 22)](https://sre.google/sre-book/addressing-cascading-failures/)

- [Deadlines (gRPC guides)](https://grpc.io/docs/guides/deadlines/)

- [Timeouts, retries, and backoff with jitter](https://builder.aws.com/content/3EumjoZascWd1oZiEgL8ORlv3qE/timeouts-retries-and-backoff-with-jitter)

- [Deadline Propagation in Agent Chains: What Happens to Your p95 SLO at Hop Three](https://tianpan.co/blog/2026/04/17/deadline-propagation-agent-chains-slo)
