---
title: "WebSocket em Go: coder/websocket vs Gorilla em Produção"
url: "https://golang.com.br/blog/websocket-go-coder-gorilla-producao/"
markdown_url: "https://golang.com.br/blog/websocket-go-coder-gorilla-producao.MD"
description: "Aprenda WebSocket em Go com coder/websocket e Gorilla: upgrade HTTP, leitura, escrita, ping, limites, autenticação, concorrência e escala em produção."
date: "2026-08-04"
author: "Golang Brasil"
---

# WebSocket em Go: coder/websocket vs Gorilla em Produção

Aprenda WebSocket em Go com coder/websocket e Gorilla: upgrade HTTP, leitura, escrita, ping, limites, autenticação, concorrência e escala em produção.


**Para implementar WebSocket em Go em 2026, use uma biblioteca mantida sobre `net/http`, mantenha uma única goroutine de escrita por conexão e trate autenticação, limites e clientes lentos como requisitos desde o início.** Para projeto novo, [`coder/websocket`](https://github.com/coder/websocket) é uma escolha enxuta e integrada a `context.Context`; ela é a continuação da biblioteca antes chamada `nhooyr/websocket`. [`gorilla/websocket`](https://github.com/gorilla/websocket) continua madura e faz sentido em sistemas que já usam sua API.

WebSocket vale a pena quando existe comunicação realmente bidirecional: chat, presença online, colaboração, dashboards interativos, jogos, terminais remotos ou notificações que também recebem comandos do cliente. Se o navegador só precisa receber eventos, Server-Sent Events (SSE) pode ser mais simples. Este guia mostra servidor, cliente, concorrência, ping, autenticação, backpressure, observabilidade e escala — o caminho entre um demo que funciona e um serviço confiável.

Se sua base HTTP ainda não está pronta, leia o [tutorial de API REST em Go](/aprenda/api-rest-go/), o guia de [middleware com net/http e Chi](/blog/middleware-go-net-http-chi-gin-producao/) e o comparativo de [frameworks HTTP em Go](/blog/frameworks-http-go-gin-echo-fiber-chi/).

## Resposta rápida: coder/websocket, Gorilla, SSE ou polling?

| Necessidade | Escolha inicial |
|---|---|
| Projeto Go novo, API pequena e uso forte de `context.Context` | **coder/websocket** |
| Sistema existente já padronizado em Gorilla | **gorilla/websocket** |
| Servidor envia eventos; navegador não envia mensagens pelo canal | **SSE** |
| Atualização rara e tolerância a alguns segundos | **Polling HTTP** |
| Comunicação bidirecional contínua e baixa latência | **WebSocket** |
| Cliente não é navegador e o contrato é RPC interno | Avalie **gRPC streaming** |

A decisão não deve nascer de um benchmark de “echo server”. Em produção, o custo costuma estar em serialização, rede, fan-out, filas, autenticação, persistência e clientes lentos. A biblioteca importa, mas o desenho do ciclo de vida importa mais.

## Como WebSocket se encaixa no net/http

Uma conexão começa como uma requisição HTTP normal. Cliente e servidor negociam um **upgrade** para WebSocket; depois do handshake, a conexão passa a transportar frames de texto, binários, ping, pong e close. Isso significa que sua rota de upgrade ainda participa das preocupações HTTP:

- TLS (`wss://`) no proxy ou na aplicação;
- cookies, sessão e cabeçalhos;
- validação de `Origin` para clientes de navegador;
- rate limit de novas conexões;
- logs e tracing do handshake;
- timeout antes do upgrade;
- readiness e graceful shutdown do servidor.

Depois do upgrade, porém, não existe uma nova requisição HTTP para cada mensagem. Autorização, limites, deadline e telemetria por operação precisam ser modelados no protocolo da aplicação.

## Servidor mínimo com coder/websocket

O exemplo abaixo cria uma rota de eco com limite de leitura, timeout por operação e fechamento explícito:

```go
package main

import (
	"context"
	"log/slog"
	"net/http"
	"os"
	"time"

	"github.com/coder/websocket"
	"github.com/coder/websocket/wsjson"
)

type Message struct {
	Type string `json:"type"`
	Text string `json:"text"`
}

func websocketHandler(logger *slog.Logger) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
			// Em produção, mantenha a validação de Origin habilitada
			// e configure padrões explicitamente quando necessário.
		})
		if err != nil {
			logger.Warn("websocket upgrade recusado", "err", err)
			return
		}
		defer conn.Close(websocket.StatusNormalClosure, "conexão encerrada")

		conn.SetReadLimit(16 << 10) // 16 KiB por mensagem

		for {
			ctx, cancel := context.WithTimeout(r.Context(), 45*time.Second)
			var incoming Message
			err := wsjson.Read(ctx, conn, &incoming)
			cancel()
			if err != nil {
				status := websocket.CloseStatus(err)
				logger.Info("websocket fechado", "status", status, "err", err)
				return
			}

			ctx, cancel = context.WithTimeout(r.Context(), 5*time.Second)
			err = wsjson.Write(ctx, conn, Message{
				Type: "echo",
				Text: incoming.Text,
			})
			cancel()
			if err != nil {
				logger.Warn("falha ao escrever websocket", "err", err)
				return
			}
		}
	}
}

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	mux := http.NewServeMux()
	mux.Handle("GET /ws", websocketHandler(logger))

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}

	logger.Info("servidor iniciado", "addr", srv.Addr)
	if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		logger.Error("servidor encerrado", "err", err)
		os.Exit(1)
	}
}
```

Instalação:

```bash
go get github.com/coder/websocket
```

O `wsjson` é conveniente para mensagens JSON pequenas. Para payload binário, streaming ou controle fino de alocação, use as operações de leitura e escrita da conexão diretamente. Não envie structs arbitrárias do domínio sem definir um contrato versionável.

## Protocolo da aplicação: não mande “qualquer JSON”

WebSocket define transporte, não o formato das mensagens. Um contrato simples pode usar um envelope:

```json
{
  "type": "chat.message.send",
  "request_id": "req_01J...",
  "version": 1,
  "payload": {
    "room_id": "room_123",
    "text": "Olá!"
  }
}
```

E a resposta:

```json
{
  "type": "chat.message.accepted",
  "request_id": "req_01J...",
  "payload": {
    "message_id": "msg_456"
  }
}
```

Inclua pelo menos:

1. **`type`** para roteamento explícito;
2. **versão** quando o contrato precisará evoluir;
3. **correlation/request ID** para resposta e observabilidade;
4. **payload limitado e validado**;
5. formato de erro previsível, sem stack trace para o cliente.

Aplique o mesmo rigor de uma [API REST com contrato](/blog/openapi-go-oapi-codegen-contratos/): documente tipos, tamanhos, autorização e compatibilidade. OpenAPI não descreve WebSocket diretamente tão bem quanto HTTP request/response, então um documento Markdown, JSON Schema, AsyncAPI ou tipos compartilhados podem fazer parte do contrato.

## Concorrência: um reader e um writer por conexão

O erro mais comum em servidores WebSocket é permitir que várias goroutines escrevam diretamente na mesma conexão. Mesmo quando uma biblioteca oferece alguma proteção, a ordem de mensagens, backpressure e encerramento ficam difíceis de raciocinar.

Use uma estrutura de cliente com canal de saída limitado:

```go
type Client struct {
	conn *websocket.Conn
	send chan Message
}

func (c *Client) writeLoop(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case msg, ok := <-c.send:
			if !ok {
				return nil
			}

			writeCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
			err := wsjson.Write(writeCtx, c.conn, msg)
			cancel()
			if err != nil {
				return err
			}
		}
	}
}
```

Outras goroutines publicam com um `select` não bloqueante ou com timeout:

```go
func (c *Client) enqueue(msg Message) bool {
	select {
	case c.send <- msg:
		return true
	default:
		return false // cliente lento; aplique a política definida
	}
}
```

Esse `false` exige uma decisão de produto:

- descartar atualização substituível, como “digitando…”;
- manter apenas o estado mais recente;
- desconectar o cliente lento;
- persistir o evento para recuperação posterior;
- sinalizar ressincronização obrigatória.

Nunca deixe um cliente com rede ruim bloquear o broadcast de todos. O canal também não deve ser ilimitado: fila sem limite apenas transforma lentidão em consumo crescente de memória.

Para aprofundar o modelo, revise [concorrência em Go](/aprenda/concorrencia-go/), [channels e goroutines em produção](/blog/channels-go-comunicacao-goroutines-producao/), [worker pools](/blog/worker-pool-go-fila-jobs/) e [context com timeout e cancelamento](/blog/context-timeout-cancelamento-go/).

## Hub, salas e broadcast

Um chat ou dashboard normalmente mantém um hub com comandos de registro, remoção e publicação. Centralizar mutações em uma goroutine reduz locks e deixa a propriedade do estado clara:

```go
type Hub struct {
	register   chan *Client
	unregister chan *Client
	broadcast  chan Message
	clients    map[*Client]struct{}
}

func (h *Hub) Run(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			return
		case client := <-h.register:
			h.clients[client] = struct{}{}
		case client := <-h.unregister:
			delete(h.clients, client)
			close(client.send)
		case msg := <-h.broadcast:
			for client := range h.clients {
				if !client.enqueue(msg) {
					delete(h.clients, client)
					close(client.send)
				}
			}
		}
	}
}
```

Em um sistema real, não faça broadcast global se as mensagens pertencem a salas, contas ou tenants. Indexe conexões por canal e confirme a autorização **na inscrição e em cada mudança de sala**. Identidade autenticada não significa permissão para observar qualquer recurso.

## coder/websocket vs Gorilla WebSocket

As duas bibliotecas implementam o protocolo e funcionam com `net/http`, mas expõem modelos diferentes.

| Critério | coder/websocket | gorilla/websocket |
|---|---|---|
| Histórico | Continuação do antigo `nhooyr/websocket` | Projeto tradicional do ecossistema Gorilla |
| Contexto | API desenhada em torno de `context.Context` | Deadlines configuradas na conexão |
| Estilo | API menor e idiomática | API madura, explícita e muito conhecida |
| JSON | Pacote auxiliar `wsjson` | `ReadJSON` e `WriteJSON` |
| Base existente | Ótima para adoção nova | Excelente quando já padronizada |
| Migração | Exige avaliar imports e comportamento | Não há razão para trocar só por moda |

Um servidor equivalente com Gorilla começa assim:

```go
var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {
		return r.Header.Get("Origin") == "https://app.exemplo.com.br"
	},
}

func handler(w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		return
	}
	defer conn.Close()

	conn.SetReadLimit(16 << 10)
	for {
		var msg Message
		if err := conn.ReadJSON(&msg); err != nil {
			return
		}
		if err := conn.WriteJSON(msg); err != nil {
			return
		}
	}
}
```

Não copie esse eco como arquitetura final: adicione deadline, ping/pong, writer único, autenticação, logs e shutdown. Na Gorilla, leia com atenção a documentação sobre concorrência: a aplicação deve respeitar o número permitido de readers e writers concorrentes.

**Recomendação prática:** não migre uma base estável de Gorilla apenas para modernizar o nome da dependência. Para projeto novo, faça um spike pequeno com coder/websocket e valide proxy, cliente, limite, close codes e carga. Escolha a API que o time consegue operar corretamente.

## Ping, pong e conexões zumbis

TCP aberto não garante que o cliente esteja utilizável. Celular troca de rede, notebook suspende, proxy encerra conexão ociosa e processo morre sem um close limpo. Você precisa detectar conexões zumbis.

A estratégia depende da biblioteca, mas o princípio é:

1. envie ping em intervalo menor que o timeout do proxy;
2. limite o tempo para receber pong ou concluir a operação;
3. encerre e remova a conexão quando o prazo vencer;
4. adicione jitter para não disparar todos os pings no mesmo milissegundo.

Não use ping a cada segundo sem necessidade. Além de tráfego, milhares de conexões sincronizadas criam picos de CPU. Documente os tempos considerando Cloudflare, load balancer, ingress Kubernetes e qualquer proxy entre cliente e aplicação.

O cliente também precisa de reconexão com **exponential backoff + jitter**. Reconectar imediatamente em loop durante um incidente produz uma tempestade que impede a recuperação. Se mensagens não podem ser perdidas, WebSocket sozinho não oferece durabilidade: use IDs, confirmação, replay ou uma fonte persistente.

## Autenticação, Origin e segurança

WebSocket não remove as regras de segurança da web.

### Autentique no handshake

Para navegador, prefira cookie de sessão `HttpOnly`, `Secure` e `SameSite` adequado, ou um ticket de conexão curto obtido previamente por HTTPS. Token em query string pode parar em access logs, histórico e ferramentas de observabilidade. Se não houver alternativa, use ticket descartável com expiração de segundos, não o token principal da conta.

### Valide Origin

Um site malicioso aberto no navegador da vítima pode tentar iniciar uma conexão para seu domínio usando cookies existentes. Valide `Origin` contra uma allowlist. Não configure `InsecureSkipVerify`, `CheckOrigin: return true` ou equivalente apenas para “fazer funcionar” em produção.

### Autorize cada ação

O usuário autenticado pode enviar `subscribe`, `send_message` ou `admin.command`. Valide o tipo, o payload e a permissão. Nunca confie em `user_id` enviado pelo cliente; derive a identidade da sessão associada à conexão.

### Limite recursos

- tamanho máximo de mensagem;
- taxa de mensagens por conexão e por conta;
- número de conexões por IP/usuário;
- quantidade de inscrições em salas;
- tempo máximo de escrita;
- comprimento e profundidade do JSON.

Combine com [rate limiting em Go](/blog/rate-limiting-go-api-producao/), [autenticação e autorização](/blog/autenticacao-autorizacao-go-apis/) e o tutorial de [segurança em aplicações Go](/tutoriais/go-security/).

## Graceful shutdown sem derrubar tudo de uma vez

No deploy, o servidor deve parar de aceitar conexões novas, sinalizar encerramento para as existentes e aguardar um prazo limitado. Um fluxo saudável:

1. readiness passa a falhar;
2. load balancer remove a instância;
3. novas conexões deixam de entrar;
4. servidor envia close com motivo de reinício;
5. writers terminam mensagens em andamento;
6. conexões restantes são canceladas no deadline;
7. processo fecha recursos e sai.

O cliente deve interpretar o fechamento e reconectar a outra instância com backoff. Evite depender de uma mensagem final obrigatória: a rede pode cair antes de entregá-la. Veja o guia completo de [graceful shutdown em Go](/blog/graceful-shutdown-go-producao/) e os padrões de [health checks](/blog/health-checks-go-liveness-readiness-startup/).

## Escala horizontal e múltiplas instâncias

Enquanto existe uma instância, um `map` em memória resolve salas pequenas. Com várias réplicas, clientes da mesma sala podem cair em pods diferentes. Você precisa de uma camada de distribuição:

- Redis Pub/Sub para eventos efêmeros;
- Redis Streams, NATS JetStream ou Kafka quando replay/durabilidade importam;
- broker gerenciado para reduzir operação;
- serviço dedicado de realtime quando o problema justificar.

Sticky session reduz mudanças de instância, mas não resolve broadcast entre pods, deploy, falha ou autoscaling. Também não transforme Redis Pub/Sub em garantia de entrega: se o consumidor estiver desconectado, o evento efêmero pode ser perdido. Para eventos importantes, use persistência e cursor.

Separe duas responsabilidades:

1. **conexão local:** socket, fila limitada, ping, autenticação;
2. **distribuição:** publicar eventos entre instâncias e recuperar estado.

O comparativo de [Kafka, NATS, RabbitMQ e SQS](/blog/mensageria-go-rabbitmq-kafka-nats-sqs/) ajuda a escolher; para replay com Redis, veja [Redis Streams em Go](/blog/redis-streams-go-filas-consumer-groups/).

## Observabilidade que realmente ajuda

Não registre o payload inteiro de cada mensagem. Além de custo e ruído, isso pode vazar token, conversa privada e dados pessoais. Prefira métricas e logs estruturados:

**Métricas:**

- conexões ativas por instância;
- upgrades aceitos e recusados;
- conexões por close code;
- mensagens recebidas/enviadas por tipo;
- bytes recebidos/enviados;
- tamanho e ocupação das filas de saída;
- clientes desconectados por lentidão;
- latência de processamento;
- erros do broker e reconexões.

**Logs:**

- `connection_id`, `user_id` interno não sensível, tenant e instância;
- motivo de rejeição do handshake;
- close code e duração da conexão;
- erro categorizado, não payload bruto;
- request/correlation ID para comandos importantes.

Amostre logs repetitivos e exponha métricas no padrão do [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/). Para logging consistente, use [slog e campos estruturados](/blog/slog-go-logging-estruturado/).

## Testes: protocolo, corrida e cliente lento

Use `httptest.Server` para subir o handler e conectar um cliente real. Cubra:

- handshake autorizado e não autorizado;
- Origin permitido e bloqueado;
- mensagem válida, inválida e grande demais;
- close normal e inesperado;
- timeout sem pong;
- cliente que não lê;
- reconexão e replay, se existir;
- shutdown com conexões ativas.

Rode também:

```bash
go test -race ./...
go test -count=100 ./internal/realtime/...
```

O race detector encontra acesso concorrente ao hub, mapas e estado da conexão. Testes repetidos ajudam a expor flakes de timing, mas prefira sincronização explícita a `time.Sleep` arbitrário. O guia de [testes de tabela em Go](/blog/testes-tabela-go-guia-table-driven-tests/) mostra como estruturar casos; para investigar travamentos, use [runtime/trace](/blog/go-tool-trace-runtime-trace-latencia-goroutines/) e [pprof](/blog/pprof-go-producao/).

## Checklist de produção

- [ ] Existe uma única goroutine de escrita por conexão?
- [ ] A fila de saída é limitada e há política para cliente lento?
- [ ] O tamanho de mensagem está limitado?
- [ ] O handshake autentica e valida `Origin`?
- [ ] Cada ação verifica autorização no servidor?
- [ ] Pings, timeouts e reconexão usam intervalos documentados e jitter?
- [ ] Close codes são tratados e medidos?
- [ ] O deploy faz drain com deadline?
- [ ] A arquitetura funciona com mais de uma réplica?
- [ ] Eventos importantes têm persistência, confirmação ou replay?
- [ ] Logs evitam payload e dados sensíveis?
- [ ] Testes rodam com `-race` e simulam cliente lento?

## Perguntas frequentes

### Qual biblioteca WebSocket usar em Go em 2026?

Para projetos novos, `coder/websocket` é uma opção enxuta, idiomática e integrada a `context.Context`; ela é a continuação do projeto anteriormente conhecido como `nhooyr/websocket`. Gorilla WebSocket continua madura, amplamente usada e adequada para bases que já dependem da sua API. Considere compatibilidade, modelo de concorrência, manutenção e testes, não apenas benchmarks.

### É possível criar WebSocket apenas com net/http?

O `net/http` fornece o servidor HTTP, mas não oferece uma API WebSocket de alto nível na biblioteca padrão. Use uma biblioteca mantida para validar o handshake, aplicar framing, controlar mensagens, close codes e limites, em vez de implementar o protocolo manualmente.

### WebSocket permite várias goroutines escrevendo na mesma conexão?

Não presuma que sim. Trate cada conexão com uma única goroutine responsável por escrita e envie mensagens a ela por um canal limitado. Isso evita escritas concorrentes, preserva a ordem e permite desconectar clientes lentos sem bloquear o servidor inteiro. Consulte o contrato da biblioteca escolhida.

### Quando usar WebSocket em vez de Server-Sent Events?

Use WebSocket quando cliente e servidor precisam trocar mensagens nos dois sentidos com baixa latência. Use SSE quando apenas o servidor envia atualizações ao navegador e simplicidade, reconexão HTTP e compatibilidade operacional pesam mais.

### Como autenticar uma conexão WebSocket em Go?

Autentique durante o handshake usando sessão ou token de curta duração, valide `Origin` e associe a identidade à conexão. Evite token sensível em URL. Depois do upgrade, aplique autorização por ação ou canal, limites de mensagem, timeout e revogação quando necessário.

## Conclusão

WebSocket em Go não é difícil porque o upgrade HTTP exige muitas linhas; é difícil porque conexões longas transformam concorrência, backpressure, autenticação, deploy e escala em parte do protocolo. **Comece com coder/websocket em projetos novos ou mantenha Gorilla em bases estáveis, mas padronize um reader, um writer, fila limitada, deadlines e close explícito por conexão.**

Antes de colocar em produção, teste cliente lento, reconexão e shutdown. Depois, meça conexões, filas e close codes. Continue com [middleware em Go](/blog/middleware-go-net-http-chi-gin-producao/), [context e timeouts](/blog/context-timeout-cancelamento-go/), [observabilidade](/blog/go-opentelemetry-observabilidade-tracing-metricas/) e [vagas Go no Brasil](/vagas/). Para fortalecer a base, siga também o [roadmap Go 2026](/aprenda/roadmap-go-2026/) e o [curso gratuito de Go](/aprenda/curso-golang-gratuito/).
