Amazon SQS é uma das formas mais pragmáticas de colocar trabalho assíncrono em uma aplicação Go que já roda na AWS. Em vez de operar RabbitMQ, Kafka ou NATS logo no primeiro mês, o time publica mensagens em uma fila gerenciada e deixa workers Go processarem no ritmo certo. Para produtos brasileiros com picos de tráfego, integrações financeiras, webhooks, envio de e-mail, relatórios e sincronização de dados, essa simplicidade costuma valer muito.
O ponto importante é não confundir simplicidade operacional com simplicidade de arquitetura. SQS entrega mensagens pelo menos uma vez, pode redeliver duplicatas, depende de visibility timeout bem configurado e exige idempotência no consumer. Se você ignorar esses detalhes, a fila vai funcionar em desenvolvimento e falhar justamente quando a produção tiver retry, deploy, timeout ou instabilidade em um fornecedor externo.
Este guia mostra como usar SQS em Go com AWS SDK v2: producer, consumer, long polling, delete depois do sucesso, retry, dead-letter queue, idempotência, observabilidade e testes locais. Se você ainda está comparando opções, leia também mensageria em Go: RabbitMQ, Kafka, NATS ou SQS e idempotência, retry e DLQ em Go.
Quando SQS faz sentido
SQS funciona melhor quando você precisa de uma fila de trabalho, não de um log de eventos com replay longo. Bons casos de uso:
- Processar webhooks fora da requisição HTTP.
- Enviar e-mails, notificações ou mensagens transacionais.
- Gerar relatórios e exports sob demanda.
- Sincronizar dados com CRM, antifraude, billing ou ERPs.
- Suavizar picos entre uma API Go e workers independentes.
- Rodar tarefas de baixa ou média latência sem operar broker.
Se vários times precisam consumir o mesmo evento com retenção longa, Kafka ou Kinesis podem ser melhores. Se você precisa de roteamento complexo, RabbitMQ pode ser mais expressivo. Se o objetivo é uma fila gerenciada simples para workers na AWS, SQS costuma ser a primeira escolha.
Standard ou FIFO?
SQS tem dois tipos principais de fila. A fila Standard escala muito, tem melhor throughput e entrega mensagens pelo menos uma vez, mas não garante ordem estrita. A fila FIFO preserva ordem dentro de um MessageGroupId e oferece deduplicação por janela, mas tem limitações de throughput e exige mais cuidado na modelagem.
Para a maioria dos workers de e-mail, relatório, importação e integração, comece com Standard e escreva o handler como idempotente. Escolha FIFO quando a ordem por entidade é parte do requisito de negócio: por exemplo, processar eventos de status de um mesmo pedido na sequência correta.
Producer com AWS SDK v2
O producer deve publicar uma mensagem pequena, versionada e rastreável. Evite payload gigante. Se o dado é grande, salve em S3 ou no banco e publique apenas a referência.
package fila
import (
"context"
"encoding/json"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sqs"
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
)
type PedidoCriado struct {
EventID string `json:"event_id"`
PedidoID string `json:"pedido_id"`
Versao int `json:"versao"`
}
type Producer struct {
client *sqs.Client
queueURL string
}
func NewProducer(client *sqs.Client, queueURL string) *Producer {
return &Producer{client: client, queueURL: queueURL}
}
func (p *Producer) PublicarPedidoCriado(ctx context.Context, evento PedidoCriado) error {
body, err := json.Marshal(evento)
if err != nil {
return err
}
_, err = p.client.SendMessage(ctx, &sqs.SendMessageInput{
QueueUrl: aws.String(p.queueURL),
MessageBody: aws.String(string(body)),
MessageAttributes: map[string]types.MessageAttributeValue{
"event_type": {
DataType: aws.String("String"),
StringValue: aws.String("pedido.criado"),
},
},
})
return err
}
O exemplo usa EventID porque o consumer precisa de uma chave estável para idempotência. Não use time.Now() como única identidade do evento: em retry, você quer reconhecer que a mesma intenção já foi processada.
Consumer com long polling
Um worker Go típico fica em loop lendo mensagens com long polling. Long polling reduz chamadas vazias e custo, porque o SQS segura a requisição por alguns segundos até uma mensagem chegar.
func RunConsumer(ctx context.Context, client *sqs.Client, queueURL string) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
resp, err := client.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
QueueUrl: aws.String(queueURL),
MaxNumberOfMessages: 5,
WaitTimeSeconds: 20,
VisibilityTimeout: 60,
})
if err != nil {
return err
}
for _, msg := range resp.Messages {
if err := processarMensagem(ctx, msg); err != nil {
// Sem DeleteMessage: o SQS libera a mensagem de novo depois do visibility timeout.
continue
}
_, err := client.DeleteMessage(ctx, &sqs.DeleteMessageInput{
QueueUrl: aws.String(queueURL),
ReceiptHandle: msg.ReceiptHandle,
})
if err != nil {
return err
}
}
}
}
A regra é simples: só chame DeleteMessage depois que o trabalho terminou com sucesso. Se o worker cair, tomar deploy ou perder rede antes do delete, a mensagem volta. Isso é uma vantagem quando o handler é idempotente; é um desastre quando o handler duplica cobrança, e-mail ou alteração de estado.
Visibility timeout e retry
VisibilityTimeout é o tempo em que uma mensagem fica invisível para outros consumers depois de ser recebida. Se o processamento demora 40 segundos e o timeout é 30, outro worker pode pegar a mesma mensagem antes do primeiro terminar. Configure o timeout acima do tempo normal de processamento e monitore casos lentos.
Para tarefas longas, existem duas opções. A primeira é quebrar o trabalho em etapas menores. A segunda é renovar a visibilidade com ChangeMessageVisibility enquanto o job avança. Prefira quebrar o trabalho quando possível, porque timeouts longos demais atrasam retry quando o worker morre.
SQS já faz retry por redelivery. Quando a mensagem falha muitas vezes, a configuração correta é mandar para uma dead-letter queue. A DLQ evita loop infinito e cria uma superfície clara para investigação: por que esse payload não processa? Falta dado? O fornecedor externo mudou contrato? O handler tem bug?
Idempotência no consumer
Como SQS pode entregar duplicatas, o handler deve aceitar a mesma mensagem mais de uma vez. Um padrão comum é gravar o EventID em uma tabela de eventos processados dentro da mesma transação do efeito de negócio.
func processarPedidoCriado(ctx context.Context, db *sql.DB, evento PedidoCriado) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.ExecContext(ctx, `
INSERT INTO eventos_processados (id, tipo, processado_em)
VALUES ($1, $2, now())
ON CONFLICT (id) DO NOTHING
`, evento.EventID, "pedido.criado")
if err != nil {
return err
}
// Aplique o efeito de negócio aqui: atualizar status, criar tarefa, enviar comando.
return tx.Commit()
}
Em sistemas financeiros ou de assinatura, a idempotência precisa ficar ainda mais explícita. A chave pode ser payment_id, invoice_id, external_event_id ou outra identidade de negócio que sobreviva a retry, deploy e reprocessamento.
Observabilidade mínima
Um worker SQS sem métricas vira caixa-preta. Monitore pelo menos:
- Quantidade de mensagens visíveis na fila.
- Idade da mensagem mais antiga.
- Taxa de sucesso e falha por tipo de evento.
- Tempo de processamento por handler.
- Quantidade de mensagens na DLQ.
- Número de redeliveries ou tentativas quando disponível.
Nos logs, registre event_id, message_id, event_type, attempt, duration_ms e resultado. Para tracing, propague um trace_id no payload ou em atributos da mensagem quando o produtor vem de uma requisição HTTP.
Testes locais
Para testes de unidade, não chame AWS. Envolva o cliente em uma interface pequena ou teste o handler separado do transporte. O código mais importante é a função que transforma mensagem em efeito de negócio, não o loop infinito do consumer.
Para integração local, LocalStack ajuda a validar SendMessage, ReceiveMessage, DeleteMessage, DLQ e atributos. Mesmo assim, rode pelo menos um teste em ambiente AWS real antes de confiar em timeout, IAM, políticas de fila e redrive policy.
Checklist de produção
Antes de colocar SQS em produção com Go, revise:
- O payload tem versão e identidade estável.
- O consumer é idempotente.
VisibilityTimeoutcobre o tempo normal de processamento.- A fila tem DLQ configurada com limite de tentativas.
- Logs e métricas mostram sucesso, falha, latência e backlog.
- O worker respeita
context.Contexte encerra bem no deploy. - IAM permite apenas as ações necessárias na fila correta.
- Mensagens grandes usam S3 ou banco como armazenamento auxiliar.
SQS em Go é uma boa escolha quando o time quer focar no produto e não na operação de broker. A parte difícil não é chamar SendMessage; é desenhar consumers que toleram duplicidade, falha, retry e deploy. Se você trata idempotência, DLQ e observabilidade como requisitos desde o começo, SQS vira uma peça simples e robusta para backend, plataforma e integrações na AWS.