Implementei um participante em Rust nas duas edições já encerradas da Rinha de Backend da Codecon e tirei 1º lugar nas duas. Os bugs de concorrência que os testes pegaram, o deadlock que só apareceu sob carga real, e os três fixes que fiz no orquestrador pra rodar no Windows.

A Codecon roda um desafio chamado Rinha de Backend: pega uma spec de API, dá um prazo fixo, e várias pessoas implementam a mesma coisa em linguagens diferentes contra o mesmo banco, com os mesmos limites de CPU e memória. No final um orquestrador sobe cada implementação, roda os mesmos testes de corretude e de carga em todo mundo, e cospe um ranking. Sem chute, sem "na minha máquina funciona" — todo mundo compete nas mesmas condições.
Peguei as duas edições que já tinham rodado (encurtador de URL e uma recriação simplificada do PIX) e implementei um participante em Rust pra cada uma. O repositório original já tinha Go, Node, Python e Ruby numa edição, e Crystal, C#, PHP e TypeScript na outra. Rust não estava lá.
Resultado final: primeiro lugar nas duas.
| Edição | Pontos | Corretude | Throughput | Latência |
|---|---|---|---|---|
| Encurtador de URL | 946/1000 | 449/500 | 297/300 | 200/200 |
| PIX | 914/1000 | 500/500 | 278/300 | 136/200 |
Isso não significa que Rust é "melhor" que as outras linguagens. Significa que essas duas implementações específicas, com essas escolhas específicas de arquitetura, saíram na frente dessas outras implementações específicas, num dia específico, na minha máquina específica. Rinha de Backend é assim: um retrato de uma tarde, não um paper acadêmico. Mas é um retrato divertido de fazer, e tem coisa técnica genuinamente interessante escondida nele.
O encurtador de URL
A spec é o que você imagina: criar URL curta, redirecionar, listar paginado, stats de cliques por dia/hora, QR code. O trabalho de verdade não está no CRUD. Está em três detalhes de concorrência que a suíte de testes cobra sem dó:
Idempotência por URL. Se você manda encurtar a mesma URL duas vezes, a segunda chamada tem que devolver o mesmo registro, não criar um segundo. Isso parece trivial até você jogar 20 requisições idênticas em paralelo nele. Sem nenhum tipo de lock, você cria 20 linhas com 20 códigos diferentes.
Conflito de custom_code. Se o usuário pede um código customizado que já existe, é 409. Dez requisições paralelas pedindo o mesmo código customizado: exatamente uma tem que ganhar.
Incremento atômico de clique. No redirect, cada clique tem que incrementar o contador sem perder nenhum sob 500 requisições simultâneas.
O terceiro é fácil: UPDATE urls SET click_count = click_count + 1 WHERE code = $1, sem ler e escrever depois, o Postgres cuida da atomicidade sozinho. Os dois primeiros são onde a coisa fica interessante, porque eles brigam entre si.
Minha primeira tentativa foi travar por advisory lock na própria URL antes de checar se ela já existe, pra tudo, com ou sem custom_code. Fazia sentido: serializa a criação, ninguém pisa no pé de ninguém. Só que existe um teste específico que manda 10 requisições paralelas com a mesma URL e o mesmo custom_code, e espera 1 sucesso e 9 conflitos de código (409), não 1 sucesso e 9 "ah, essa URL já existe, toma 200 de volta". Com meu lock por URL, todo mundo batia primeiro na checagem de URL, achava que já existia (porque o vencedor tinha acabado de commitar) e devolvia 200, quebrando o teste, porque o comportamento esperado é que a disputa pelo código apareça primeiro.
Fui espiar como a implementação de referência em Go resolvia isso, e a saída era mais sutil do que eu tinha pensado: a chave do lock muda dependendo se veio custom_code ou não. Com custom_code, tranca pelo código (pg_advisory_xact_lock(hashtext('code:' || $1))), checa se o código já está em uso primeiro, e só depois checa a URL. Sem custom_code, tranca pela URL direto. Copiei essa granularidade pro Rust e os dois testes, o de corrida por URL e o de corrida por código, passaram juntos.
O PIX
Essa edição é mais pesada. A API recebe uma transferência, grava como pending e responde na hora. Quem move o dinheiro de verdade é um worker rodando em background, que pega as transferências pendentes, confere se o saldo cobre, e marca completed ou failed. As invariantes que valem sob qualquer nível de concorrência: a soma de todos os saldos nunca muda, nenhuma conta fica negativa, e a mesma chave de idempotência nunca gera dois débitos.
Implementei o worker com uma dúzia de tasks assíncronas concorrentes, cada uma pegando uma transferência pendente por vez com SELECT ... FOR UPDATE SKIP LOCKED (assim nenhuma task pega a mesma transferência que outra já está processando) e travando as duas contas envolvidas, pagador e recebedor, em ordem alfabética fixa por id antes de mexer no saldo. A ordem fixa importa: sem ela, um ciclo de transferências A→B, B→C, C→A processado em paralelo pode travar cada worker esperando a conta que o outro já segurou. Deadlock clássico. Ordenando sempre pelo mesmo critério, os workers nunca disputam locks em direções opostas.
Isso funcionou nos testes de corretude locais. Quebrou no teste circular-transfers, exatamente o cenário de três contas em ciclo que eu tinha em mente ao desenhar o lock ordenado, só que rodando de verdade contra o pipeline completo, com carga de verdade. Os logs do container mostravam deadlock detected repetido dezenas de vezes.
O que eu não tinha considerado: inserir uma nova transferência também trava as contas envolvidas. payer_id e payee_id são foreign keys pra accounts, e o Postgres, pra garantir a integridade referencial, pega um lock do tipo FOR KEY SHARE nas linhas referenciadas durante o INSERT. Esse lock é leve (várias inserções concorrentes referenciando a mesma conta não brigam entre si), mas ele conflita com FOR UPDATE, que era o que meu worker usava pra travar as contas antes de debitar. E a ordem em que o INSERT pega esses locks (pagador, depois recebedor, na ordem das colunas) não tem nada a ver com a minha ordem alfabética. Resultado: o worker girando em uma direção, o INSERT girando na outra, e o Postgres detectando o ciclo e abortando uma das duas transações.
A correção foi trocar FOR UPDATE por FOR NO KEY UPDATE no worker. É um lock mais fraco: ainda serializa duas liquidações concorrentes na mesma conta, que é tudo que eu precisava, mas não conflita com o FOR KEY SHARE do INSERT, porque ele só protege contra mudanças na chave primária, que ninguém está tocando. Rodei o pipeline de carga inteiro duas vezes depois disso pra ter certeza de que não era sorte. Zero deadlock nas duas.
Os bugs que não eram meus
Depois que os testes de corretude passaram, eu queria o ranking de verdade, com throughput e latência de todo mundo, não só corretude isolada. O orquestrador (escrito em TypeScript, parte do repositório original) assume que você está rodando num devcontainer Linux. Eu estava no Windows puro. Bati em três bugs que não tinham nada a ver com meu código Rust: afetavam qualquer participante.
- O script passava a porta como
APP_PORT=3005 docker compose up -d, sintaxe de shell POSIX que ocmd.exedo Windows simplesmente não entende. Toda tentativa de subir um container morria com "'APP_PORT' não é reconhecido como um comando". - O k6 era chamado com
--out json=/dev/nullpra descartar uma saída que não era usada./dev/nullsó existe de verdade no Git Bash, que emula o caminho. Viacmd.exepuro, sem essa emulação, o k6 falhava a abrir o arquivo e abortava o teste inteiro sem rodar uma VU sequer. O resultado final aparecia como 0 requisições por segundo, erro 0%, pra todo mundo. - O modo de apresentação do orquestrador (que roda prova por prova, todos lado a lado) tinha um
try/finallysemcatchna fase de carga. Se um único participante desse um erro transitório ao subir de novo pra medir throughput isolado, a exceção não tratada derrubava o pipeline inteiro, jogando fora os resultados de corretude que já tinham levado dez minutos pra rodar.
Corrigi os três: env var de verdade em vez de prefixo no shell, tirei a flag --out que não fazia falta nenhuma, e coloquei o catch que faltava. Está tudo commitado nesse repositório: qualquer pessoa rodando o pipeline completo no Windows se beneficia, não só eu.
Rodando você mesmo
O repositório é um fork do original da Codecon, com o participante Rust adicionado nas duas edições e os três fixes acima. Pra conferir:
git clone https://github.com/AxxosTecnologia/rinha-de-backend-rust
cd rinha-de-backend-rust/editions/02-pix # ou 01-url-shortener
./scripts/test-local.sh rust # só os testes de corretude do meu participante
./scripts/rounds.sh # pipeline completo com todo mundo, ranking no finalPrecisa de Docker rodando e, pro pipeline completo, de k6 instalado. Os números que citei aqui saíram exatamente desse comando, na minha máquina, em 2026-08-26. Os arquivos brutos de resultado estão em docs/scores/ no repositório caso alguém queira auditar.