O problema: conexões WebSocket não são HTTP
Olha só, WebSocket é stateful. Cada conexão precisa de exatamente um worker dono dela. Quando esse worker morre do nada, as conexões ficam órfãs e os dados simplesmente param de fluir. Balanceamento de carga tradicional não resolve porque a coordenação tem que rolar na camada de aplicação.
Neste tutorial, você vai aprender um padrão production-ready usando conditional writes do DynamoDB como mecanismo de lease distribuído. Sem ZooKeeper, sem etcd, sem dor de cabeça.
A sacada é simples: o DynamoDB já te dá escrita atômica condicional. Isso é literalmente um lock distribuído de graça.

O ciclo de vida do lease: adquirir, renovar, liberar, expirar
Um lease é só um item no DynamoDB que diz quem é o dono da conexão e até quando. Bora ver o schema:
# Estrutura do item no DynamoDB para um lease de conexão
{
"Pk": "CONN#meeting-123", # Chave de partição: ID da conexão
"desired_state": "STARTED", # STARTED ou STOPPED
"ws_url": "wss://upstream.example.com/stream",
"lease_owner": "worker-abc", # Dono atual
"lease_expires_at_ms": 1712345678901, # Epoch ms
"last_seq": 42 # Para retomada
}
Adquirindo um lease (atômico)
A ConditionExpression é o coração da coisa. Só funciona se o lease não existe OU se já expirou:
import boto3
from botocore.exceptions import ClientError
async def try_acquire_lease(table, conn_id, worker_id, lease_seconds=20):
now_ms = int(time.time() * 1000)
expires_ms = now_ms + (lease_seconds * 1000)
try:
table.update_item(
Key={"Pk": f"CONN#{conn_id}"},
UpdateExpression="SET lease_owner = :w, lease_expires_at_ms = :e",
ConditionExpression=(
"attribute_not_exists(lease_owner) OR "
"lease_expires_at_ms < :now"
),
ExpressionAttributeValues={
":w": worker_id,
":e": expires_ms,
":now": now_ms
}
)
return True # Beleza, é nosso
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return False # Outro worker pegou
raise
Renovando (heartbeat)
A cada 5 segundos o dono renova. Se falhar, é porque perdeu a posse — hora de sair de fininho:
async def renew_lease(table, conn_id, worker_id, lease_seconds=20):
now_ms = int(time.time() * 1000)
try:
table.update_item(
Key={"Pk": f"CONN#{conn_id}"},
UpdateExpression="SET lease_expires_at_ms = :e",
ConditionExpression="lease_owner = :w",
ExpressionAttributeValues={
":e": now_ms + lease_seconds * 1000,
":w": worker_id
}
)
return True
except ClientError:
return False # Perdemos a posse
Reconciliação de órfãos
Um GSI em (desired_state, lease_expires_at_ms) permite que qualquer worker ache conexões que deveriam estar ativas mas estão sem dono:
# Consulta o GSI em busca de conexões órfãs
orphans = table.query(
IndexName="desired_state-lease_expires_at_ms-index",
KeyConditionExpression="desired_state = :s AND lease_expires_at_ms < :now",
ExpressionAttributeValues={":s": "STARTED", ":now": now_ms}
)
# Cada worker tenta adquirir; o conditional write garante que só um ganha
for item in orphans["Items"]:
await try_acquire_lease(table, item["Pk"], worker_id)
Shutdown gracioso
No SIGTERM, libera os leases na hora setando lease_expires_at_ms = 0. Aí os outros workers pegam na próxima reconciliação em vez de esperar expirar naturalmente:
async def release_lease(table, conn_id, worker_id):
table.update_item(
Key={"Pk": f"CONN#{conn_id}"},
UpdateExpression="SET lease_expires_at_ms = :zero",
ConditionExpression="lease_owner = :w",
ExpressionAttributeValues={":zero": 0, ":w": worker_id}
)

Avisos de produção e tuning
Clock skew é seu inimigo
A expiração do lease é comparada com o relógio local do worker, não com o relógio do servidor DynamoDB. No Fargate, o Amazon Time Sync Service mantém o skew em poucos milissegundos — tranquilo para um lease de 20s. Fora da AWS, confira se o NTP tá configurado ou aumente a duração do lease pelo skew máximo esperado.
Custo: a real
As escritas de heartbeat dominam o custo. A cada 5 segundos:
| Conexões | WCU/s | On-demand/mês | Provisionado/mês |
|---|---|---|---|
| 100 | 20 | ~$65 | ~$10 |
| 500 | 100 | ~$325 | ~$47 |
| 2.000 | 400 | ~$1.300 | ~$190 |
Para volumes altos e sustentados, use capacidade provisionada com Auto Scaling. Dobrar o heartbeat pra 10s corta o WCU pela metade, mas dobra a janela de failover — escolha com sabedoria.
Limites de escala
- O loop
check_desired_statepor conexão funciona pra frotas pequenas. Em escala, useBatchGetItempra cortar RCU de N leituras/ciclo pra 1. - Queries no GSI durante reconciliação usam leituras eventualmente consistentes por padrão — metade do RCU.
- Comece com
MAX_CONNECTIONS=100por task e aumente monitorando memória (cada WebSocket consome ~2-5 MB).
O que esse padrão NÃO resolve
- Replay de dados: se um worker morre no meio do stream, as mensagens entre falha e reaquisição se perdem a menos que o upstream faça buffer ou suporte retomada por offset via
last_seq. - Failover cross-region: o padrão é single-region. Multi-region exige Global Tables do DynamoDB e resolução de conflitos cuidadosa.
- Thundering herd: quando muitos leases expiram juntos, todos os workers consultam o GSI de uma vez. Adicione jitter nos intervalos de reconciliação.

Conclusão e próximos passos
Você viu como conditional writes do DynamoDB eliminam a necessidade de um serviço de coordenação separado enquanto garantem semântica de exatamente-um-dono. O ciclo completo — adquirir, renovar, liberar, reconciliar — cobre todo o espectro de falhas: crashes, deploys rolling e scale-in.
Comece por aqui: faça deploy da implementação de referência com Terraform, valide o ciclo de vida do lease com um punhado de conexões de teste, depois aumente MAX_CONNECTIONS gradualmente observando o CloudWatch Container Insights.
Próximo nível:
- Adicione tracing distribuído com AWS X-Ray pra visualizar o caminho de aquisição entre workers.
- Implemente retomada por offset usando o campo
last_seqpra fechar gaps de dados após failover. - Explore DynamoDB Streams pra disparar reconciliação de forma reativa em vez de polling.
Quer ver como isso se encaixa em padrões mais amplos de arquitetura AWS? Dá uma olhada na nossa análise de AWS Architecture Trends 2024. E se você curte como decisões de design aparentemente bobas têm implicações éticas grandes, nosso texto sobre lições de pegadinhas de 1º de abril no web design vale a leitura.
Fonte: AWS Architecture Blog