Olha só o problema: conexões WebSocket de longa duração são difíceis de gerenciar
Requisições HTTP são stateless: chegam, são processadas e retornam uma resposta. Conexões WebSocket são o oposto. São canais persistentes e bidirecionais que precisam ficar abertos, processar mensagens e responder a keep-alive pings. Esse estado introduz desafios operacionais sérios quando você roda uma frota de workers:
- Falhas de worker: quando um worker cai, ele derruba todas as suas conexões WebSocket. Sem um mecanismo para detectar a falha e reatribuir essas conexões, os dados são perdidos.
- Rolling deployments: o ECS termina tarefas antigas e inicia novas. Cada tarefa terminada derruba suas conexões, criando uma janela onde conexões ficam sem dono.
- Escalonamento horizontal: adicionar workers é fácil, mas remover exige drenar conexões e verificar que outros workers assumam antes da tarefa sair.
- Double-claiming: se dois workers acham que são donos da mesma conexão, você tem processamento duplicado ou erros de protocolo.
Como os workers são clientes WebSocket que iniciam conexões de saída, você precisa de coordenação na camada de aplicação, não na camada de rede. A solução: conditional writes do DynamoDB como lease distribuído.
Este tutorial é baseado no post do AWS Architecture Blog Building resilient real-time streaming workers with Amazon DynamoDB leases.

O padrão de lease: implementação core
Um lease é um item no DynamoDB que rastreia quem é o dono de uma conexão e quando essa posse expira. O schema da tabela:
| Atributo | Tipo | Descrição |
|---|---|---|
Pk | String (PK) | Connection ID, ex: CONN#meeting-123 |
desired_state | String | STARTED ou STOPPED |
ws_url | String | URL WebSocket upstream |
lease_owner | String | Worker ID dono da conexão |
lease_expires_at_ms | Number | Epoch ms de expiração |
last_seq | Number | Último número de sequência processado |
Um GSI em desired_state (partition key) e lease_expires_at_ms (sort key) permite consultar conexões órfãs de forma eficiente.
Acquire: conditional write como lock distribuído
A peça crítica é a ConditionExpression. Ela só tem sucesso se o lease não existe ainda (attribute_not_exists) ou já expirou (lease_expires_at_ms < :now). O DynamoDB avalia isso atomicamente, então só um worker vence.
import boto3
from botocore.exceptions import ClientError
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('connections')
def try_acquire_lease(conn_id: str, worker_id: str, lease_seconds: int = 20):
now_ms = int(time.time() * 1000)
try:
table.update_item(
Key={'Pk': f'CONN#{conn_id}'},
UpdateExpression='SET lease_owner = :w, lease_expires_at_ms = :exp',
ConditionExpression='attribute_not_exists(lease_owner) OR lease_expires_at_ms < :now',
ExpressionAttributeValues={
':w': worker_id,
':exp': now_ms + lease_seconds * 1000,
':now': now_ms,
},
)
return True
except ClientError as e:
if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
return False # outro worker já é dono
raise
Renew: loop de heartbeat
O worker dono renova seu lease a cada HEARTBEAT_EVERY segundos. Se a renovação retornar False, o worker sabe que perdeu a posse e sai limpo.
async def heartbeat_loop(conn_id: str, worker_id: str):
while not shutdown_event.is_set():
ok = await renew_lease(conn_id, worker_id)
if not ok:
print(f'Lease perdido para {conn_id}, saindo')
return
await asyncio.sleep(HEARTBEAT_EVERY) # 5 segundos
Release: shutdown graceful
Durante o shutdown graceful, defina lease_expires_at_ms = 0 para que outros workers peguem a conexão no próximo ciclo de reconciliação em vez de esperar a expiração natural.
async def graceful_shutdown():
shutdown_event.set()
await asyncio.gather(*[
release_lease(conn_id, worker_id) for conn_id in active_connections
])

Reconciliação de órfãos e escalonamento
O loop de reconciliação é a rede de segurança. A cada 60 segundos, cada worker consulta o GSI por registros onde desired_state = STARTED e lease_expires_at_ms < now. Essas são conexões órfãs — o dono anterior caiu sem liberar. Qualquer worker saudável pode chamar try_acquire_lease nelas. Como o acquire usa conditional write, múltiplos workers podem rodar reconciliação concorrentemente sem risco.
Budget de tempo de recuperação: 20s de expiração do lease + até 60s de intervalo de reconciliação = ~80s no pior caso. O shutdown graceful reduz isso para um ciclo de reconciliação.
Escalonando com métricas do CloudWatch
Cada worker publica uma métrica customizada ActiveConnections. Uma política de target tracking do Application Auto Scaling escala a frota baseada na média. Scale-in é seguro porque o SIGTERM dispara o shutdown graceful, liberando leases para reaquisição imediata.
Otimização de custo
Escritas de heartbeat dominam o custo: um update_item por conexão a cada 5s = 1 WCU. Com 500 conexões, isso dá ~$325/mês on-demand vs ~$47/mês provisioned. Para contagens altas sustentadas, use capacidade provisionada com Auto Scaling. Dobrar o intervalo de heartbeat para 10s corta o consumo de WCU pela metade — só dobre a duração do lease para manter a proporção 4:1.
⚠️ Limitações e cuidados
- Clock skew: a expiração do lease usa o
nowfornecido pelo worker, não um relógio server-side. O AWS Fargate fornece Amazon Time Sync, mantendo o skew em milissegundos, mas se você deployar fora da AWS, verifique NTP e aumente a duração do lease pelo skew máximo esperado. - Não é um lock client genérico: a biblioteca
amazon-dynamodb-lock-clientexiste para Java, mas não integra estado de domínio no registro do lock. Este padrão combina lock + metadata em um item para reduzir reads. - SQS sozinho não basta: o SQS entrega uma unidade de trabalho para um consumidor, mas a posse de WebSocket é um estado contínuo que precisa ser renovado. O SQS não consegue consultar conexões sem dono ativo.
Próximos passos
- Adicione tracing distribuído com AWS X-Ray para visibilidade end-to-end entre workers.
- Implemente lógica de reconexão com replay upstream ou retomada por offset usando
last_seqpara lidar com gaps de dados. - Para insights mais profundos sobre minimizar custos de inferência em sistemas distribuídos similares, veja Stop Wasting GPU Cycles: Why Code Agents Beat Tool-Calling for In-Game AI.

Principais aprendizados
- Conditional writes do DynamoDB são um lock distribuído — sem serviço de coordenação externo. A
ConditionExpressionnoupdate_itemgarante que exatamente um worker é dono de cada conexão. - Heartbeat + reconciliação cobre todo o espectro de falhas — expiração de lease lida com crashes, shutdown graceful lida com deploys, e novos workers adquirem leases enquanto workers saindo os liberam.
- Este padrão se aplica amplamente — transcrição em tempo real, ingestão IoT, feeds financeiros, streaming de eventos ao vivo — qualquer sistema que gerencie conexões WebSocket de longa duração em escala.
Leitura relacionada
Se você está otimizando performance de infraestrutura cloud, talvez goste também de PostgreSQL on Azure Just Got a Serious Upgrade Inside VS Code — um deep dive em melhorias de tooling de banco de dados que combinam bem com os padrões de backend discutidos aqui.
A implementação completa — worker application, Lambda event router e templates Terraform — está disponível no repositório de samples da AWS. Faça o deploy da infraestrutura e valide o ciclo de vida do lease com um pequeno conjunto de conexões de teste antes de escalar.