Push e pull, e o que cada um custa
Existem só duas formas de o seu código descobrir que uma mensagem chegou. Ou o outro lado te avisa, ou você pergunta. Tudo o mais — uma biblioteca cliente com um waitFor nela, um SDK que “transmite” uma caixa de entrada, um helper de teste que bloqueia — é uma dessas duas coisas com a maquinaria escondida, e vale a pena saber qual delas você tem nas mãos antes de precisar depurá-la.
Quatro arranjos cobrem quase tudo o que existe por aí.
- Um webhook
- O serviço faz uma requisição HTTP para um endereço que você controla, cada vez que um e-mail chega. É a espera mais barata que existe — você não faz nada até haver algo a fazer — e o preço é um endereço na internet pública, um listener de pé no exato momento em que o e-mail chega, um segredo compartilhado para provar que a requisição veio deles, e uma resposta sua própria para o que acontece quando o seu listener não estava lá.
- Um long poll
- Você faz a requisição e o servidor a mantém aberta até que o e-mail chegue ou um timeout expire. Não exige nada de você além de uma conexão de saída, e custa ao servidor um worker por espera — motivo pelo qual todo serviço que oferece isso limita tanto a duração da espera quanto o número de esperas simultâneas.
- Um polling simples
- Você pergunta, repetidamente, e cada requisição é respondida na hora com o que houver ali. É o único arranjo que funciona a partir de um laptop atrás de um roteador, de um runner de CI sem rota de entrada, e de um agente rodando dentro do sandbox de outra pessoa — e é o assunto inteiro deste guia.
- Um protocolo de caixa de entrada
- O IMAP tem o
IDLE, que é um long poll com outra roupagem: a conexão fica aberta e o servidor anuncia o e-mail novo por ela. É genuinamente parecido com push, e exige uma caixa de entrada com credenciais, um cliente capaz de manter um socket aberto e reconectar quando ele cai, e um servidor que respeita o comando — o que é uma quantidade enorme de maquinaria para um job que precisa de uma mensagem.
Colocadas lado a lado, a escolha acaba sendo menos sobre elegância e mais sobre o que cada uma exige da máquina em que o seu código está rodando.
| O que isso exige de você | O webhook | O polling |
|---|---|---|
| Um endereço onde o seu código possa ser encontrado | Sim: uma URL pública com certificado, roteável a partir da internet. | Não. Uma única requisição de saída é toda a exigência. |
| Um segredo para guardar e rotacionar | Sim: uma chave de assinatura, ou um estranho pode te enviar uma mensagem falsa. | Não. Não há nada para verificar, porque nada chega sem ter sido pedido. |
| Algo rodando no momento em que o e-mail chega | Sim — e quando está fora do ar, se você chega a receber a mensagem depende da política de retentativa do remetente, não da sua decisão. | Não. Nada se perde enquanto você não está olhando: a caixa de entrada guarda a mensagem por 5 dias de qualquer forma. |
| Requisições feitas quando não há e-mail | Nenhuma. Essa é toda a vantagem. | Uma por intervalo — o custo real, e o assunto do resto deste guia. |
O loop que todo mundo escreve primeiro
São quatro linhas, funciona no dia em que é escrito, e cada um dos seus problemas aparece depois e em outro lugar: em um pipeline às três da manhã, em um agente que está “pensando” há onze minutos, em uma caixa de entrada que responde 429 a um colega porque o seu loop está consumindo todo o orçamento.
import time
import requests
while True:
r = requests.get("https://grabmail.io/api/v1/mailbox",
params={"address": "signup-42@grabmail.io"})
if r.json()["messages"]:
break
time.sleep(1)Cinco coisas estão erradas nele, e só a primeira é óbvia.
- Ele nunca desiste
- Não há prazo, então quando a mensagem realmente não vai chegar — o formulário rejeitou o endereço, a fila do remetente travou, alguém digitou o domínio errado — esse loop não falha. Ele trava. Um job que trava é peor do que um que falha, porque o log termina sem nunca dizer o motivo.
- Ele conta tentativas e chama isso de segundos
- Mesmo com um limite no número de voltas, trinta tentativas de “um segundo” nunca são trinta segundos: cada volta também custa uma requisição, e uma requisição que leva 400 ms transforma os seus trinta segundos em quarenta e dois. Acrescente uma retentativa e a aritmética deixa de ser aritmética.
- Todo runner pergunta no mesmo tick
- Inicie vinte jobs a partir do mesmo pipeline e eles vão fazer polling em sincronia, porque todos começaram com poucos milissegundos de diferença entre si e todos dormem o mesmo segundo inteiro. O pico é vinte vezes a média, e é o pico que é recusado.
- Ele pega a mensagem mais nova, não a sua
- A primeira entrada da lista é o que estiver no topo daquela caixa de entrada, o que em um endereço público pode ser o e-mail de outra pessoa, e em um endereço reutilizado é o da semana passada. Um loop que sai ao ver a primeira mensagem vai sair tranquilamente antes mesmo de a mensagem esperada ter chegado.
- Ele trata toda resposta como sucesso
- Ler a lista de mensagens a partir de um
429ou de um404gera um erro três frames longe de qualquer coisa que o explique, e ler a partir de um500pode não gerar erro nenhum. O código de status é a primeira coisa a olhar, não a última.
Pare pelo relógio, não pela contagem
Tome o prazo uma única vez, antes da primeira requisição, a partir de um relógio monotônico — um que não pode voltar no tempo quando a máquina corrige o próprio horário — e compare com ele no início de cada volta. Tudo o mais no loop fica então livre para mudar sem alterar a duração da espera: você pode alargar o intervalo, repetir uma recusa, ou acrescentar um segundo filtro, e noventa segundos continuam sendo noventa segundos.
Quanto tempo é tempo suficiente é uma pergunta sobre o remetente, não sobre você. Um e-mail que uma máquina gera em resposta a um formulário costuma ser entregue em segundos, quase sempre menos de dez; uma fila com atraso acumulado, um destinatário que faz greylisting, ou um lote de hora em hora, são de uma ordem de grandeza diferente, e nenhum intervalo que você escolher faz o e-mail chegar mais rápido.
| O que você está esperando | Um prazo honesto | O que fazer quando ele passar |
|---|---|---|
| Um e-mail de cadastro ou verificação, dentro de um teste | 60 a 120 segundos | Falhe o teste e imprima o endereço. Em nove de cada dez vezes a caixa de entrada está vazia porque o formulário recusou o endereço, e o endereço é a primeira coisa que quem lê o log precisa ver. |
| Uma redefinição de senha que uma pessoa acabou de pedir | 30 a 60 segundos | Avise que ainda não chegou e ofereça reenviar. Não fique girando atrás de uma tela silenciosa: a pessoa vai pedir um segundo código de qualquer jeito, e agora existem dois códigos. |
| Um agente concluindo um cadastro por conta própria | Duas ou três esperas do lado do servidor, então de 50 a 75 segundos | Diga isso na resposta. “Nenhum e-mail de confirmação depois de um minuto” é um resultado sobre o qual o agente pode agir; uma chamada de ferramenta que nunca retorna não é. |
| Uma newsletter, um recibo, qualquer coisa em lote | Minutos — ou nem espere | Em vez disso, faça polling em um horário fixo e deixe o processo terminar. Algo parado em um socket por dez minutos é algo que vai ser matado por um proxy, um runner ou um limite de container. |
O prazo também é o lugar honesto para colocar a sua mensagem de erro. “Nada correspondendo a ‘Confirm your email’ chegou em signup-42@grabmail.io dentro de 90 s” nomeia o endereço, o filtro e o orçamento, que são três das quatro coisas necessárias para entender o que aconteceu. A quarta — o que de fato chegou — também vale a pena imprimir: uma lista dos assuntos que o loop viu e rejeitou transforma “isso é instável” em “o assunto mudou” em uma única leitura.
Com que frequência perguntar, e quando alargar o intervalo
O piso é o que o serviço permitir, e aqui é uma requisição por segundo, por endereço. Isso não é um desincentivo — fazer polling uma vez por segundo é o padrão pretendido, não há cota diária, nem cota mensal, nem crédito de rajada para gerenciar — mas é um piso, e um loop que pergunta duas vezes dentro do mesmo segundo recebe um 429 na segunda vez, em vez de uma resposta mais rápida.
- Um intervalo fixo
- Um segundo, sempre, até o prazo acabar. Perfeitamente adequado para uma espera que vai terminar em dez segundos, e o padrão certo para um único teste em um único runner. Sua única falha é continuar perguntando no mesmo ritmo bem depois de já estar óbvio que o e-mail não vai chegar.
- Um intervalo que se alarga
- Um segundo enquanto a mensagem provavelmente ainda está em trânsito, depois dobrando — dois, quatro, oito — com um teto. Isso custa um pouco de latência em uma mensagem que chega atrasada, e economiza a maior parte das requisições em uma espera que ia falhar de qualquer jeito. Coloque um teto: um intervalo que dobra sem teto passa a segunda metade de um prazo de dois minutos dormindo.
- Jitter, somado e nunca subtraído
- Espalhe as voltas com uma fração aleatória, para que vinte runners parem de perguntar no mesmo tick. A receita usual — um valor aleatório entre zero e o intervalo — está errada aqui, porque metade da sua faixa cai abaixo do piso de um segundo. Em vez disso, some a aleatoriedade por cima: o intervalo é um mínimo, e o jitter só pode atrasar uma volta, nunca antecipá-la.
- Uma pausa que não é sua para escolher
- Quando a resposta é um
429, o intervalo é o que oRetry-Afterdisser, e a volta que foi recusada não contou como tentativa. Contá-la como uma faz com que um loop sendo limitado passe o prazo inteiro colecionando recusas sem nunca ter lido a caixa de entrada.
Quinze segundos a um segundo, depois dobrando até um teto de oito, com jitter por cima, encaixa quase toda espera deste guia em seis linhas:
def delay(attempt: int) -> float:
# One second while the message is probably still in flight, then
# wider. Never below a second: the list endpoint allows one call
# per second, per address, so jitter is added and never taken off.
step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
return step + random.uniform(0.0, step / 2)O expoente é deslocado para que o alargamento comece depois do trecho fixo, e não a partir da primeira volta. Sem esse deslocamento, o intervalo já teria chegado a oito segundos no momento em que um e-mail de cadastro lento chegasse, e uma espera que deveria durar doze segundos passaria a durar vinte.
Nada disso vale para a primeira requisição. Pergunte imediatamente, antes de dormir: uma mensagem que já estava na caixa de entrada quando o loop começou — o caso normal para qualquer coisa disparada antes de a espera começar — não deveria custar nem um segundo de latência para ser percebida.
Lendo uma recusa
Toda resposta do endpoint de listagem é JSON, e as que não são uma caixa de entrada compartilham um formato: um slug error, que é estável e é o que deve orientar a decisão, e uma message, que é texto livre e pode ser reformulada a qualquer momento. Uma recusa por excesso de velocidade também carrega um header:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json; charset=utf-8
{"error":"rate_limited","message":"one request per second, per address"}O Retry-After vem em segundos inteiros, e é o número real — tirado de quanto ainda resta do orçamento daquele endereço, não de uma constante na documentação. Dormir exatamente esse tempo é ao mesmo tempo a coisa mais educada e a mais rápida a fazer: uma pausa mais curta é recusada de novo, uma mais longa é tempo desperdiçado. Aqui está tudo que um loop de polling pode encontrar, e o que cada resposta está realmente pedindo dele.
| O que volta | O que significa | O que o loop deve fazer |
|---|---|---|
200 com count: 0 | A caixa de entrada existe e está vazia. Essa é a resposta normal durante a maior parte de uma espera. | Continue esperando. Não é um erro, e nunca vai se tornar um. |
429 — erro rate_limited | Rápido demais: uma segunda requisição de listagem dentro do mesmo segundo para este endereço, ou mais de 1,200 requisições em um minuto a partir desta origem. | Durma pelos segundos indicados em Retry-After, depois pergunte de novo. Não conte a recusa como uma tentativa. |
404 — erro unknown_domain | A parte depois do @ não é hospedada aqui. Quase sempre é um erro de digitação, ou um domínio cujo registro MX nunca foi apontado para aqui. | Pare. Nenhuma quantidade de espera corrige um domínio. Imprima o endereço que te foi passado. |
400 — erro invalid_address | O parâmetro address está ausente, tem mais de 320 caracteres, ou não está no formato nome@domínio. | Pare. Isso é um erro de quem chamou, e vai ser o mesmo erro em toda volta. |
400 — erro bad_cursor | O valor de before não tem o formato de um id de mensagem. Um id bem formado, mas expirado, não é esse erro: ele responde 200 com uma página vazia. | Pare de paginar e comece de novo pela primeira página. |
404 — erro not_found, de uma mensagem específica | Aquele id não está naquela caixa de entrada — ou estava, e desde então expirou ou foi apagado. | Trate como algo que se perdeu, não como algo atrasado. Um id que você viu em uma listagem segundos atrás não vai voltar. |
500 — erro storage_failed | Algo falhou do nosso lado ao ler a caixa de entrada. | Pergunte de novo, mas deixe o prazo mandar, e não pergunte mais rápido do que o normal. |
Duas dessas sete significam pare, e são as duas que vale a pena destacar. Um loop que trata unknown_domain como “ainda não” gasta noventa segundos inteiros para provar algo que o serviço já tinha dito nos primeiros quarenta milissegundos.
Qual mensagem é sua
Uma caixa de entrada não é uma fila, e a coisa mais nova nela não é necessariamente a que você está esperando. Em um domínio público, qualquer um que adivinhar o endereço pode enviar para ele; em uma suíte de testes o mesmo endereço é frequentemente reutilizado entre execuções; e um único cadastro costuma enviar duas mensagens — uma de boas-vindas e uma de confirmação — das quais só uma carrega o código. A solução é um watermark, e ele precisa ser registrado antes da coisa que causa o e-mail.
- Antes de enviar o formulário, liste a caixa de entrada com
limit=1e guarde o id da mensagem mais nova, ou nada, se estiver vazia. Esse id é o watermark. - Faça a coisa — envie o formulário, chame o endpoint, clique no botão.
- Consulte a lista. As mensagens voltam da mais nova para a mais antiga, então desça a partir do topo e pare no instante em que encontrar o watermark: tudo dali para baixo é mais antigo que a sua ação e pode ser ignorado sem ser lido.
- Filtre o que está acima dele pelo remetente, pelo assunto, ou por ambos. Uma substring geralmente basta, e deve ser a parte que não vai ser localizada — um teste que procura por “Confirm your email” falha no dia em que a conta usada no teste é trocada para outro idioma.
- Só então, e não antes, abra a mensagem. A listagem carrega um
previewcurto, não o corpo, e o código que você procura está frequentemente depois do fim dele. Mais uma requisição traz a mensagem completa, e ela é cobrada contra um orçamento separado e muito maior do que o da listagem.
Em um shell, essas duas leituras se parecem com isto — primeiro o watermark, depois a consulta:
curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=1"
curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=25"As duas requisições nomeiam o endereço completo, porque aqui o endereço é a caixa de entrada: não há sessão, não há cursor guardado em seu nome, e nada em uma chamada que a próxima se lembre. É também por isso que é seguro observar um endereço a partir de dois lugares ao mesmo tempo — ler não consome nada, então dois loops na mesma caixa de entrada veem cada mensagem, e nenhum consegue tirar uma mensagem de debaixo do outro.
Agindo exatamente uma vez
Uma consulta que é repetida pode ver a mesma mensagem duas vezes, e isso não é raro: o servidor responde, a conexão cai antes de o corpo chegar até você, o seu cliente HTTP tenta de novo, e a segunda resposta contém a mensagem que a primeira já trazia. Se o que você faz com uma mensagem é clicar em um link, confirmar um pagamento ou postar em um canal, fazer isso duas vezes é um bug com consequências fora do seu processo.
- Guarde os ids que você já processou
- Um conjunto de ids em memória basta para uma espera que nasce e morre dentro de uma única função. Para qualquer coisa que precise sobreviver a um reinício — uma caixa de entrada esvaziada por um job agendado, um agente trabalhando em um backlog — isso precisa ser anotado em algum lugar que sobreviva junto.
- Apagar é idempotente
- Apagar uma mensagem responde
200tanto na segunda vez quanto na primeira, então uma exclusão repetida nunca parece uma falha e nunca precisa de um caso especial. Apague depois de ter agido, e não antes: uma queda entre as duas etapas custa então uma releitura, que é recuperável, em vez da mensagem, que não é. - O id aqui não é o Message-ID do remetente
- O id da API é nosso: é restrito a uma caixa de entrada, e deixa de existir quando a mensagem expira. O header
Message-IDé do remetente, viaja com a mensagem, e é o que você quer usar se estiver correspondendo o mesmo e-mail entre dois sistemas — o guia sobre headers mostra onde encontrá-lo.
Nada disso é necessário para um teste que espera por um código e depois descarta a caixa de entrada. Tudo isso passa a ser necessário no momento em que um loop passa a rodar sem supervisão, porque a falha que ele evita não parece uma falha: parece o trabalho sendo feito, duas vezes, corretamente.
Quando a espera é responsabilidade do servidor
Existe um único lugar aqui onde você não escreve o loop, e ele existe para quem não pode se dar ao luxo de escrever um. Um agente de IA paga por cada turno que gasta verificando, então uma ferramenta que responde “ainda não” nove vezes são nove turnos de nada. O wait_for_message do servidor MCP mantém a requisição aberta, faz o polling do nosso lado, e responde uma única vez — com a mensagem, ou com uma declaração simples de que esperou e nada chegou.
$ curl -sX POST https://grabmail.io/mcp -H "Content-Type: application/json" -d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"wait_for_message","arguments":{
"address":"signup-42@grabmail.io","subject_contains":"code",
"timeout_seconds":25}}}'Quatro coisas sobre isso vale a pena saber antes de construir em cima dele.
- Ele espera no máximo 25 segundos
timeout_secondspode pedir menos, nunca mais. O teto não é arbitrário: cada espera ocupa um worker que não faz nada além de dormir, e uma requisição mantida aberta por minutos é uma requisição que morre no timeout de algum proxy bem antes de retornar.- Ele filtra na entrada
from_contains,subject_containsesince_idsão as mesmas três decisões da seção anterior, só que tomadas no servidor.since_idé o watermark, e ele importa mais aqui do que em qualquer outro lugar: sem ele a chamada retorna imediatamente com o que já estiver na caixa de entrada.- Um timeout é uma resposta, não um erro
- Quando nada chega, ele retorna
timed_outdefinido, junto com quanto tempo realmente esperou, e diz em bom português que chamar de novo é como você continua esperando. Duas ou três chamadas são uma espera normal para um e-mail de cadastro: esse é o loop, e são três turnos em vez de noventa. - Existem 8 vagas de espera, e nenhuma fila
- Quando todas estão ocupadas, a chamada volta na hora e diz isso, em vez de entrar em uma fila atrás de outros sete agentes. É a falha certa: um agente que recebe “muitas esperas em andamento” pode listar a caixa de entrada e seguir em frente, enquanto um agente parado em uma fila só pode ficar parado.
O limite por endereço continua valendo por dentro disso — o nosso loop está sujeito ao mesmo limite de taxa que o seu, então uma espera do lado do servidor não é um jeito de contornar o piso, só um jeito de parar de pagar por ele em turnos. Para uma suíte de testes nada disso vale o esforço: um teste já é um processo que tem permissão para dormir, e um loop na linguagem em que o teste foi escrito é muito mais fácil de depurar do que um loop remoto. O guia de MCP cobre o resto das ferramentas.
O loop completo, de uma vez
Tudo o que veio antes, em um único arquivo: um prazo tirado de um relógio monotônico, um intervalo que se alarga com jitter de um lado só, Retry-After respeitado e não contado como tentativa, um watermark para decidir o que é novo, um filtro no assunto, e uma requisição extra para buscar a mensagem que a listagem só mostra em preview.
import random
import time
import requests
API = "https://grabmail.io/api/v1"
ADDRESS = "signup-42@grabmail.io"
def delay(attempt: int) -> float:
# One second while the message is probably still in flight, then
# wider. Never below a second: the list endpoint allows one call
# per second, per address, so jitter is added and never taken off.
step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
return step + random.uniform(0.0, step / 2)
def watermark(s):
# Read this BEFORE the form is submitted. Every id above it
# afterwards is mail that arrived because of what you did.
r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 1})
r.raise_for_status()
seen = r.json()["messages"]
return seen[0]["id"] if seen else None
def wait_for(s, subject, since, timeout=120.0):
deadline = time.monotonic() + timeout
attempt = 0
rejected = set()
while time.monotonic() < deadline:
r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 25})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1")))
continue # refused, so it was not an attempt
if r.status_code == 200:
for m in r.json()["messages"]: # newest first
if m["id"] == since:
break # older than the watermark
if subject.lower() in m["subject"].lower():
full = s.get(f"{API}/message/{m['id']}",
params={"mailbox": ADDRESS})
full.raise_for_status()
return full.json()
rejected.add(m["subject"])
elif r.status_code < 500:
raise RuntimeError(r.json().get("error", r.status_code))
# a 5xx falls through: transient, and the deadline still governs
time.sleep(delay(attempt))
attempt += 1
raise TimeoutError(
f"nothing matching {subject!r} at {ADDRESS} in {timeout:.0f}s; "
f"saw {sorted(rejected) or 'nothing at all'}")São, de propósito, cinquenta e poucas linhas de biblioteca padrão e um cliente HTTP. Não há nada para instalar, nada para configurar, e nenhum segredo em lugar nenhum — e esse é o ponto: a mesma forma se move sem mudanças para o Node, para um script shell, ou para o que quer que o seu framework de teste já use para fazer requisições.
- Leia o watermark antes da ação que causa o e-mail, nunca depois.
- Pergunte uma vez, imediatamente, e só então durma. Nunca durma primeiro.
- Tire o prazo de um relógio monotônico, e verifique-o no início de cada volta.
- Mantenha o intervalo em um segundo por endereço, ou mais, e adicione jitter só para cima.
- Durma exatamente o que o
Retry-Afterdisser, e não conte uma recusa como tentativa. - Decida com base no slug
error:unknown_domaineinvalid_addresssignificam pare, não espere. - Filtre por remetente ou assunto, e pare de percorrer a lista ao alcançar o watermark.
- Abra a mensagem antes de fazer o parse dela — a listagem carrega um preview, não o corpo.
- Falhe informando o endereço, o filtro, o orçamento, e os assuntos que o loop rejeitou.
Nove regras, e oito delas existem por causa de uma falha que alguém teve que reconstruir a partir de um log. A que não é sobre falha é a segunda: perguntar uma vez antes da primeira pausa é o que faz uma espera por uma mensagem que já chegou durar quatro milissegundos em vez de um segundo — o que, em uma suíte de duzentos testes, são três minutos de tempo real que depois ninguém precisa explicar.
Perguntas
O GrabMail tem webhook?
Não, e isso não é uma lacuna esperando para ser preenchida. O serviço recebe e-mail e o expõe por HTTP sem chave: não há uma conta atrás de um endereço público à qual anexar um callback, e nenhuma fila para guardar uma entrega que o seu endpoint recusou. Se o seu fluxo genuinamente não pode fazer polling, a página de comparação lista os serviços que oferecem um.
Com que frequência posso fazer polling em um endereço?
Uma vez por segundo, por endereço — e esse é o padrão pretendido, não o limite extremo dele. Não há cota diária, nem cota mensal, nem crédito de rajada para gerenciar. Vinte caixas de entrada consultadas uma vez por segundo a partir de um runner é uso normal; o único outro teto é 1,200 requisições por minuto a partir de uma única origem, que é exatamente essas vinte, e não uma vigésima primeira.
Por que o meu loop devolveu uma mensagem de uma execução de teste anterior?
Porque ele pegou a primeira entrada da lista sem perguntar quando ela chegou. Uma caixa de entrada guarda tudo o que foi enviado para ela por 5 dias, e um endereço reutilizado está cheio da execução anterior. Leia o id mais novo antes de disparar o e-mail e ignore tudo a partir dele para baixo — ou apague o conteúdo da caixa de entrada no início do teste, o que é uma requisição por mensagem e remove a ambiguidade por completo.
Uma caixa de entrada vazia é um 404?
Não. Uma caixa de entrada vazia é 200 com count: 0 e uma lista vazia, de propósito, para que um loop de polling nunca precise tratar “ainda não” como um caso especial. Um 404 do endpoint de listagem significa que o domínio não é hospedado aqui; um 404 de uma mensagem específica significa que aquele id não está naquela caixa de entrada, ou expirou.
Quanto tempo devo esperar por um e-mail de verificação?
De sessenta a cento e vinte segundos em um teste automatizado, de trinta a sessenta para uma pessoa esperando na tela. A maioria dos e-mails gerados por máquina chega em menos de dez segundos; a cauda longa pertence à fila do remetente, não à entrega. Se isso costuma ficar perto do seu prazo, um prazo maior não é a resposta — alguma outra coisa está errada.
Dois processos podem fazer polling no mesmo endereço ao mesmo tempo?
Sim. Ler não consome nada, então os dois veem cada mensagem, e nenhum esconde e-mail do outro. Os dois, porém, compartilham o orçamento de uma requisição por segundo daquele endereço, então dois loops perguntando todo segundo vão ser recusados cerca de metade das vezes cada um: dê dois segundos para cada um, ou deixe um fazer o polling e passar os resultados para o outro.
Devo fazer polling, ou usar a ferramenta de espera via MCP?
Faça polling, se você está escrevendo um teste ou um script: um processo que tem permissão para dormir deveria dormir, e um loop na sua própria linguagem é mais fácil de depurar do que um loop remoto. Use wait_for_message quando quem chama paga por turno em vez de por segundo, o que na prática significa um agente de IA. Ele espera até 25 segundos por chamada, filtra por remetente e assunto, e devolve um timeout simples que você pode simplesmente chamar de novo.


