---
id: case-serverless-events
title: "Serverless xử lý event an toàn"
shortTitle: "Serverless event"
track: cases
trackTitle: "Tình huống vận hành"
order: 14
level: important
status: published
estimatedMinutes: 40
prerequisites:
  - serverless-functions
  - case-outbox
next: case-outage
reviewedAgainst: 2026-10-12
sources:
  - title: "AWS Lambda error handling and retries"
    url: https://docs.aws.amazon.com/lambda/latest/dg/invocation-retries.html
  - title: "Azure Functions error handling"
    url: https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-error-pages
  - title: "Azure Functions idempotent design"
    url: https://learn.microsoft.com/en-us/azure/azure-functions/functions-idempotent
---

# Serverless xử lý event an toàn

Serverless function là phần compute chạy handler của bạn. RabbitMQ và Kafka là hệ messaging. Function có thể được kích hoạt bởi một queue hay một event stream, nhưng điều đó không biến broker thành serverless.

## Để làm gì

Bạn sẽ xử lý retry tự động, duplicate, poison message và concurrency tăng đột biến, mà không làm chết database phía sau.

## Gồm những gì

- Invocation đồng bộ và bất đồng bộ.
- Retry do event source quyết định, không chỉ do code của bạn.
- DLQ, idempotency và reserved concurrency.

Bài không deploy lên tài khoản cloud nào, mà chỉ thiết kế luồng xử lý.

## Dùng khi nào

Lambda xử lý message từ SQS hoặc event từ S3; Azure Functions xử lý Service Bus, timer hoặc Event Hubs. HTTP function đồng bộ có semantics khác hẳn queue trigger.

## Kiến thức cốt lõi

`try/catch` thôi là chưa đủ. Nếu bạn catch lỗi rồi chỉ ghi log, platform coi invocation đó là thành công và xóa message. Nếu bạn throw, platform có thể retry. Hành vi cụ thể phụ thuộc vào loại trigger:

- HTTP: caller nhận lỗi và tự quyết định có retry hay không.
- Queue: message bị ẩn trong visibility timeout, rồi xuất hiện lại nếu chưa được ACK hoặc xóa.
- Stream: checkpoint không vượt qua được batch bị lỗi, nên một record độc có thể chặn cả partition.
- Async event: platform có lịch retry riêng, cùng destination hoặc DLQ theo cấu hình.

Vì retry diễn ra tự động, function phải idempotent. Hãy dùng event ID làm khóa trong bảng inbox, hoặc dùng conditional write. "Function chỉ chạy đúng một lần" không phải là điều platform đảm bảo.

Concurrency tăng theo backlog. 1.000 invocation cùng mở connection tới PostgreSQL có thể làm database chết, dù function vẫn còn quota. Hãy đặt reserved concurrency hoặc max concurrency theo connection budget, hoặc dùng proxy và connection pool do nền tảng cung cấp. Queue sẽ giữ backlog giúp bạn; database không cần phải nhận hết trong một giây.

Poison message là event luôn fail vì schema hoặc dữ liệu sai. Sau một số lần nhận, hãy đưa nó vào DLQ. Đặt alert theo DLQ và tuổi message, và chỉ replay sau khi code đã xử lý được schema đó.

Nếu trigger hỗ trợ partial batch response, platform chỉ retry những record bị lỗi thay vì chạy lại cả batch. Hãy đọc tài liệu của event source cụ thể trước khi bật.

Timeout của function phải nhỏ hơn visibility timeout của queue, cộng thêm một khoảng an toàn. Nếu function chạy 60 giây mà message hiện lại sau 30 giây, sẽ có hai instance cùng xử lý một message.

Khi backlog tăng hoặc dữ liệu bị trùng, điều tra theo thứ tự: số connection của database trước, rồi số message trong DLQ, rồi tới cặp timeout của function và visibility timeout. Connection đầy cho biết concurrency đang vượt sức chịu của downstream. DLQ tăng cho biết có message độc. Còn timeout lệch nhau là nguyên nhân phổ biến của duplicate, dù không có lỗi nào trong log.

## Lệnh và code mẫu

Pseudo-code:

```js
export async function handler(event) {
  for (const message of event.messages) {
    await db.transaction(async (tx) => {
      if (await tx.inbox.has(message.id)) return;
      await applyBusinessChange(tx, message);
      await tx.inbox.insert(message.id);
    });
  }
}
```

Đừng catch rồi bỏ qua. Lỗi tạm thời phải được throw để trigger retry. Lỗi dữ liệu mà retry không sửa được cần được đánh dấu thất bại, để policy đưa message vào DLQ khi vượt giới hạn.

Ví dụ connection budget:

```text
Database cho app worker: 80 connection
Mỗi invocation tối đa: 1 connection
Reserved concurrency an toàn ban đầu: 40
Phần còn lại cho API, migration và thao tác vận hành
```

## Thực hành

1. Visibility timeout của queue là 30 giây, timeout của function là 60 giây. Chỉ ra lỗi: message có thể được giao lại trong khi invocation đầu tiên vẫn đang chạy.
2. Database dành 80 connection cho worker. Chọn concurrency 40 thay vì 1.000, và giải thích phần dự phòng còn lại dùng cho việc gì.
3. Gửi cùng một event ID hai lần. Dữ liệu nghiệp vụ chỉ được thay đổi một lần.
4. Một message sai schema đi vào DLQ sau giới hạn retry, còn các message tốt phía sau vẫn chạy bình thường.

## Kết quả mong đợi

Retry không tạo ra side effect trùng lặp. Backlog có thể tăng nhưng database không bị connection storm. Poison message không chặn luồng xử lý mãi mãi.

## Checklist

- [ ] Biết trigger retry khi nào và tối đa bao nhiêu lần.
- [ ] Handler idempotent theo event ID.
- [ ] Concurrency tính theo sức chịu của downstream, không theo quota của function.
- [ ] Visibility timeout lớn hơn thời gian xử lý tối đa, có khoảng an toàn.
- [ ] DLQ có alert và runbook replay.

## Lỗi thường gặp

**Catch mọi exception rồi return success.** Platform xóa message. Log vẫn ghi lỗi nhưng dữ liệu đã mất. Chỉ catch những lỗi mà bạn đã xử lý xong.

**Tăng concurrency để giảm backlog.** Database đầy connection, function bị timeout, và backlog lại tăng nhanh hơn. Hãy giới hạn concurrency và xử lý chỗ nghẽn thật sự.

**Replay toàn bộ DLQ sau khi sửa một lỗi schema.** Một số message đã chạy một phần side effect trước khi fail. Replay phải idempotent và đi theo từng batch nhỏ.

## Nguồn

1. [Lambda retries](https://docs.aws.amazon.com/lambda/latest/dg/invocation-retries.html) — retry khác nhau theo invocation.
2. [Azure Functions error handling](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-error-pages) — retry policy và poison message theo binding.
3. [Idempotent Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-idempotent) — duplicate và idempotency.

## Tiếp theo

[App tự sập, điều tra nguyên nhân](./15-outage.md)
