Push et pull, et ce que chacun coûte
Il n’y a que deux façons pour votre code de découvrir qu’un message est arrivé. Soit l’autre partie vous le dit, soit vous demandez. Tout le reste — une bibliothèque cliente avec un waitFor dedans, un SDK qui « diffuse » une boîte de réception, un utilitaire de test qui bloque — n’est que l’un de ces deux-là avec la mécanique cachée, et ça vaut le coup de savoir lequel vous tenez avant d’avoir à le déboguer.
Quatre dispositifs couvrent presque tout ce qui existe.
- Un webhook
- Le service effectue une requête HTTP vers une adresse que vous possédez, chaque fois que du courrier arrive. C’est l’attente la moins chère possible — vous ne faites strictement rien jusqu’à ce qu’il y ait quelque chose à faire — et le prix à payer, c’est une adresse sur l’internet public, un service à l’écoute au moment même où le courrier arrive, un secret partagé pour prouver que la requête vient bien d’eux, et votre propre réponse à la question de ce qui se passe quand votre récepteur n’était pas là.
- Un long poll
- Vous faites la requête et le serveur la garde ouverte jusqu’à ce que du courrier arrive ou qu’un délai expire. Cela ne demande rien de votre part hormis une connexion sortante, et cela coûte au serveur un worker par connexion en attente — c’est pourquoi tout service qui en propose un plafonne à la fois la durée de l’attente et le nombre d’attentes simultanées.
- Une interrogation simple
- Vous interrogez, à répétition, et chaque requête reçoit immédiatement une réponse avec ce qu’il y a. C’est le seul dispositif qui fonctionne depuis un ordinateur portable derrière un routeur, depuis un exécuteur CI sans route entrante, et depuis un agent qui tourne dans le bac à sable de quelqu’un d’autre — et c’est tout le sujet de ce guide.
- Un protocole de boîte
- IMAP dispose d’
IDLE, qui est un long poll déguisé : la connexion reste ouverte et le serveur y annonce l’arrivée de courrier. C’est authentiquement du push, et cela demande une boîte avec des identifiants, un client capable de garder une socket ouverte et de se reconnecter quand elle tombe, et un serveur qui respecte la commande — beaucoup de mécanique pour une tâche qui n’a besoin que d’un seul message.
Mises côte à côte, ces options se distinguent moins par leur élégance que par ce que chacune exige de la machine sur laquelle tourne votre code.
| Ce que ça exige de vous | Un webhook | Interrogation |
|---|---|---|
| Une adresse où votre code peut être atteint | Oui : une URL publique avec un certificat, routable depuis l’internet. | Non. Une seule requête sortante, et c’est toute l’exigence. |
| Un secret à conserver et à faire tourner | Oui : une clé de signature, sans quoi n’importe qui peut vous poster un faux message. | Non. Il n’y a rien à vérifier, parce que rien n’arrive jamais sans avoir été demandé. |
| Quelque chose qui tourne au moment où le courrier arrive | Oui — et quand c’est en panne, le fait même de recevoir le message dépend de la politique de nouvelle tentative de l’expéditeur, pas de vous. | Non. Rien n’est manqué pendant que vous ne regardez pas : la boîte le conserve 5 jours de toute façon. |
| Des requêtes faites alors qu’il n’y a pas de courrier | Aucune. C’est tout l’intérêt. | Une par intervalle — le vrai coût, et le sujet de tout le reste de ce guide. |
La boucle que tout le monde écrit en premier
Elle fait quatre lignes, elle marche le jour où elle est écrite, et chacun de ses problèmes se révèle plus tard et ailleurs : dans un pipeline à trois heures du matin, dans un agent qui « réfléchit » depuis onze minutes, dans une boîte qui répond 429 à un collègue parce que votre boucle retient tout le budget.
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)Cinq choses ne vont pas avec elle, et seule la première est évidente.
- Elle n’abandonne jamais
- Il n’y a pas d’échéance, donc quand le message ne vient vraiment pas — le formulaire a rejeté l’adresse, la file de l’expéditeur est bloquée, quelqu’un a mal tapé le domaine — cette boucle ne plante pas. Elle reste bloquée. Une tâche qui reste bloquée est pire qu’une tâche qui plante, parce que le journal s’arrête sans jamais dire pourquoi.
- Elle compte des tentatives et les appelle des secondes
- Même avec une limite sur le nombre de passages, trente tentatives à « une seconde » ne font jamais trente secondes : chaque passage coûte aussi une requête, et une requête qui prend 400 ms transforme vos trente secondes en quarante-deux. Ajoutez une nouvelle tentative et l’arithmétique cesse d’être de l’arithmétique.
- Chaque exécuteur interroge au même instant
- Lancez vingt tâches du même pipeline et elles interrogent au pas de charge, parce qu’elles ont toutes démarré à quelques millisecondes les unes des autres et qu’elles font toutes une pause de la même seconde entière. Le pic vaut vingt fois la moyenne, et c’est le pic qui se fait refuser.
- Elle prend le message le plus récent, pas le vôtre
- La première entrée de la liste est tout simplement ce qui se trouve en haut de cette boîte, ce qui sur une adresse publique peut être le courrier de quelqu’un d’autre, et sur une adresse réutilisée est celui de la semaine dernière. Une boucle qui s’arrête au premier message qu’elle voit s’arrêtera sans problème avant même que celui qu’elle attendait ne soit arrivé.
- Elle traite chaque réponse comme une réussite
- Lire la liste de messages à partir d’un
429ou d’un404lève une erreur à trois niveaux de tout ce qui pourrait l’expliquer, et la lire à partir d’un500peut ne rien lever du tout. Le code de statut est la première chose à regarder, pas la dernière.
Arrêtez-vous sur une horloge, pas sur un compte
Prenez l’échéance une seule fois, avant la première requête, à partir d’une horloge monotone — une horloge qui ne peut pas revenir en arrière quand la machine corrige son heure — et comparez-vous à elle au début de chaque passage. Tout le reste dans la boucle est alors libre de changer sans changer la durée de l’attente : vous pouvez élargir l’intervalle, retenter un refus, ou ajouter un second filtre, et quatre-vingt-dix secondes restent quatre-vingt-dix secondes.
Combien de temps est suffisant est une question qui concerne l’expéditeur, pas vous. Le courrier qu’une machine génère en réponse à un formulaire est en général livré en quelques secondes à un seul chiffre ; une file avec du retard accumulé, un destinataire qui pratique le greylisting, ou un lot horaire, c’est un ordre de grandeur différent, et aucun intervalle que vous choisirez ne le fera arriver plus vite.
| Ce que vous attendez | Une échéance honnête | Que faire quand elle passe |
|---|---|---|
| Un email d’inscription ou de vérification, dans un test | 60 à 120 secondes | Faites échouer le test et affichez l’adresse. Neuf fois sur dix, la boîte est vide parce que le formulaire a refusé l’adresse, et l’adresse est la première chose que quiconque lit le journal a besoin de voir. |
| Une réinitialisation de mot de passe qu’une personne vient de demander | 30 à 60 secondes | Dites-lui qu’il n’est pas arrivé et proposez de le renvoyer. Ne continuez pas à faire tourner une roue derrière un écran silencieux : la personne en redemandera un de toute façon, et il y aura maintenant deux codes. |
| Un agent qui termine une inscription seul | Deux ou trois attentes côté serveur, donc 50 à 75 secondes | Dites-le dans la réponse. « Pas d’email de confirmation après une minute » est un résultat sur lequel l’agent peut agir ; un appel d’outil qui ne revient jamais n’en est pas un. |
| Une newsletter, un reçu, tout ce qui est traité par lot | Des minutes — ou pas d’attente du tout | Interrogez plutôt selon un calendrier et laissez le processus se terminer. Quelque chose qui reste assis sur une socket pendant dix minutes est quelque chose qui se fera tuer par un proxy, un exécuteur ou une limite de conteneur. |
L’échéance est aussi l’endroit honnête où placer votre message d’erreur. « Rien de correspondant à “Confirmez votre email” n’est arrivé à signup-42@grabmail.io en 90 s » nomme l’adresse, le filtre et le budget, ce qui fait trois des quatre choses nécessaires pour comprendre ce qui s’est passé. La quatrième — ce qui est arrivé — vaut aussi la peine d’être affichée : une liste des objets que la boucle a vus et rejetés transforme « c’est instable » en « l’objet a changé » en une seule lecture.
À quelle fréquence demander, et quand élargir
Le plancher est ce que le service autorise, et ici c’est une requête par seconde, par adresse. Ce n’est pas un découragement — interroger une fois par seconde est le modèle prévu, il n’y a ni quota journalier, ni quota mensuel, ni crédit de rafale à gérer — mais c’est un plancher, et une boucle qui demande deux fois dans la même seconde obtient un 429 pour la seconde requête plutôt qu’une réponse plus rapide.
- Un intervalle fixe
- Une seconde, chaque fois, jusqu’à l’échéance. Parfaitement adapté à une attente qui sera terminée en dix secondes, et le bon choix par défaut pour un seul test sur un seul exécuteur. Son seul défaut est qu’il continue à demander au même rythme longtemps après qu’il est devenu évident que le courrier ne vient pas.
- Un intervalle croissant
- Une seconde tant que le message est probablement encore en transit, puis un doublement — deux, quatre, huit — avec un plafond. Cela coûte un peu de latence sur un message qui arrive en retard, et cela économise la plupart des requêtes dans une attente qui allait échouer de toute façon. Plafonnez-le : un intervalle qui double sans plafond passe toute la seconde moitié d’une échéance de deux minutes à dormir.
- De l’aléa, ajouté et jamais retiré
- Écartez les passages d’une fraction aléatoire pour que vingt exécuteurs arrêtent de demander au même instant. La recette habituelle — une valeur aléatoire entre zéro et l’intervalle — est ici la mauvaise, parce que la moitié de sa plage tombe sous le plancher d’une seconde. Ajoutez plutôt l’aléa par-dessus : l’intervalle est un minimum, et l’aléa ne fait jamais que retarder un passage.
- Une pause qui n’est pas à vous de choisir
- Quand la réponse est un
429, l’intervalle est celui queRetry-Afterindique, et le passage qui a été refusé n’était pas une tentative. Comptez-le comme telle et une boucle bridée passera toute son échéance à accumuler des refus sans jamais avoir lu la boîte.
Quinze secondes à une seconde, puis un doublement jusqu’à un plafond de huit, avec de l’aléa par-dessus, fait entrer presque toutes les attentes de ce guide dans six lignes :
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)L’exposant est décalé pour que la croissance commence après la portion fixe plutôt qu’à partir du premier passage. Sans ce décalage, l’intervalle a déjà atteint huit secondes au moment où un email d’inscription lent arrive, et une attente qui aurait dû prendre douze secondes en prend vingt.
Rien de tout cela ne s’applique à la première requête. Demandez immédiatement, avant toute pause : un message qui était déjà dans la boîte quand la boucle a démarré — le cas normal pour tout ce qui a été déclenché avant que l’attente ne commence — ne devrait pas coûter une seconde de latence à remarquer.
Lire un refus
Chaque réponse du point de terminaison de liste est du JSON, et celles qui ne sont pas une boîte partagent une seule forme : un slug error, qui est stable et sur lequel il faut brancher la logique, et un message, qui est du texte et peut être reformulé à tout moment. Un refus pour excès de vitesse porte aussi un en-tête :
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"}Retry-After est exprimé en secondes entières, et c’est le vrai chiffre — tiré de ce qu’il reste réellement du budget de cette adresse, pas d’une constante dans la documentation. Faire une pause exactement de cette durée est à la fois le plus poli et le plus rapide à faire : une pause plus courte se fait refuser à nouveau, une pause plus longue est du temps donné pour rien. Voici tout ce qu’une boucle d’interrogation peut rencontrer, et ce que chaque réponse lui demande vraiment.
| Ce qui revient | Ce que ça signifie | Ce que la boucle doit faire |
|---|---|---|
200 avec count: 0 | La boîte existe et elle est vide. C’est la réponse normale pendant la plus grande partie d’une attente. | Continuez d’attendre. Ce n’est pas une erreur, et ça n’en devient jamais une. |
429 — rate_limited | Trop rapide : une deuxième requête de liste en moins d’une seconde pour cette adresse, ou plus de 1,200 requêtes en une minute depuis cette source. | Faites une pause de Retry-After secondes, puis demandez à nouveau. Ne comptez pas le refus comme une tentative. |
404 — unknown_domain | La partie après le @ n’est pas hébergée ici. Presque toujours une faute de frappe, ou un domaine dont l’enregistrement MX n’a jamais été pointé ici. | Arrêtez. Aucune attente ne répare un domaine. Affichez l’adresse qu’on vous a donnée. |
400 — invalid_address | Le paramètre address est manquant, dépasse 320 caractères, ou n’a pas la forme nom@domaine. | Arrêtez. C’est une faute de l’appelant, et ce sera la même faute à chaque passage. |
400 — bad_cursor | La valeur before n’a même pas la forme d’un identifiant de message. Un identifiant bien formé mais expiré n’est pas cette erreur : il répond 200 avec une page vide. | Arrêtez la pagination et repartez de la première page. |
404 — not_found, pour un message | Cet identifiant n’est pas dans cette boîte — ou il y était, et il a depuis expiré ou été supprimé. | Traitez-le comme disparu plutôt que comme en retard. Un identifiant vu dans une liste il y a quelques secondes ne reviendra pas. |
500 — storage_failed | Quelque chose a échoué de notre côté pendant la lecture de la boîte. | Redemandez, mais laissez l’échéance gouverner et ne demandez pas plus vite que d’habitude. |
Deux de ces sept réponses veulent dire stop, et ce sont les deux qui méritent d’être signalées bruyamment. Une boucle qui traite unknown_domain comme « pas encore » passe quatre-vingt-dix secondes entières à prouver quelque chose que le service lui avait dit dans les quarante premières millisecondes.
Quel message est le vôtre
Une boîte n’est pas une file, et ce qu’elle contient de plus récent n’est pas forcément ce que vous attendez. Sur un domaine public, quiconque devine l’adresse peut lui envoyer du courrier ; dans une suite de tests, la même adresse est souvent réutilisée entre les exécutions ; et une seule inscription envoie fréquemment deux messages — un message de bienvenue et une confirmation — dont un seul porte le code. Le remède, c’est un repère, et il doit être pris avant la chose qui provoque le courrier.
- Avant de soumettre le formulaire, listez la boîte avec
limit=1et conservez l’identifiant du message le plus récent, ou rien du tout si elle est vide. Cet identifiant est le repère. - Faites l’action — soumettez le formulaire, appelez le point de terminaison, cliquez sur le bouton.
- Interrogez la liste. Les messages reviennent du plus récent au plus ancien, donc parcourez-la depuis le haut et arrêtez-vous dès que vous rencontrez le repère : tout ce qui est en dessous est plus ancien que votre action et peut être ignoré sans être lu.
- Filtrez ce qui se trouve au-dessus sur l’expéditeur, l’objet, ou les deux. Une sous-chaîne suffit généralement, et ce devrait être la partie qui ne sera pas localisée — un test qui recherche « Confirmez votre email » échoue le jour où le compte testé passe à une autre langue.
- Alors, et seulement alors, ouvrez-le. La liste porte un court
previewet non le corps, et le code que vous recherchez est très souvent situé après la fin de celui-ci. Une requête supplémentaire récupère le message entier, et elle est imputée sur un budget séparé, bien plus large que celui de la liste.
Dans un shell, ces deux lectures ressemblent à ceci — d’abord le repère, puis l’interrogation :
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"Les deux requêtes nomment l’adresse en entier, parce qu’ici l’adresse est la boîte : il n’y a ni session, ni curseur conservé pour vous, ni rien d’un appel que le suivant se rappellerait. C’est aussi pourquoi il est sûr de surveiller une adresse depuis deux endroits à la fois — lire ne consomme rien, donc deux boucles sur la même boîte voient chacune tous les messages, et ni l’une ni l’autre ne peut en retirer un sous le nez de l’autre.
Agir exactement une fois
Une requête d’interrogation qui est rejouée peut voir le même message deux fois, et ce n’est pas un événement rare : le serveur répond, la connexion tombe avant que le corps ne vous parvienne, votre client HTTP retente, et la seconde réponse contient le message que la première portait déjà. Si ce que vous faites avec un message, c’est cliquer sur un lien, confirmer un paiement ou publier dans un canal, le faire deux fois est un bug dont les conséquences dépassent votre processus.
- Conservez les identifiants déjà traités
- Un ensemble d’identifiants en mémoire suffit pour une attente qui naît et meurt à l’intérieur d’une seule fonction. Pour tout ce qui doit survivre à un redémarrage — une boîte vidée par une tâche planifiée, un agent qui traite un arriéré — il faut l’écrire quelque part qui survit avec lui.
- Supprimer est idempotent
- Supprimer un message répond
200la seconde fois comme la première, donc une suppression rejouée ne ressemble jamais à un échec et n’a jamais besoin d’un cas particulier. Supprimez après avoir agi plutôt qu’avant : un plantage entre les deux vous coûte alors une relecture, ce qui est récupérable, au lieu du message, qui ne l’est pas. - L’identifiant ici n’est pas le Message-ID de l’expéditeur
- L’identifiant de l’API est le nôtre : il est propre à une boîte, et il cesse d’exister quand le message expire. L’en-tête
Message-IDest celui de l’expéditeur, il voyage avec le message, et c’est celui qu’il vous faut si vous faites correspondre le même courrier entre deux systèmes — le guide sur les en-têtes dit où le trouver.
Rien de tout cela n’est nécessaire pour un test qui attend un code puis jette la boîte. Tout cela devient nécessaire dès qu’une boucle tourne sans surveillance, parce que l’échec qu’elle évite ne ressemble pas à un échec : il ressemble au travail fait, deux fois, correctement.
Quand l’attente revient au serveur
Il y a un seul endroit ici où vous n’écrivez pas la boucle, et il existe pour les appelants qui ne peuvent pas s’en permettre une. Un agent IA paie pour chaque tour qu’il consacre à vérifier, donc un outil qui répond « rien pour l’instant » neuf fois, ce sont neuf tours pour rien. Le wait_for_message du serveur MCP garde à la place la requête ouverte, interroge de notre côté, et répond une seule fois — soit avec le message, soit avec une déclaration simple disant qu’il a attendu et que rien n’est venu.
$ 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}}}'Quatre choses à ce sujet valent la peine d’être connues avant de vous appuyer sur lui.
- Il attend 25 secondes au maximum
timeout_secondspeut en demander moins, jamais plus. Le plafond n’est pas arbitraire : chaque connexion en attente est un worker qui ne fait rien d’autre que dormir, et une requête maintenue ouverte pendant des minutes est une requête qui meurt sur le délai d’un proxy de quelqu’un bien avant de revenir.- Il filtre à l’entrée
from_contains,subject_containsetsince_idsont les trois mêmes décisions que dans la section ci-dessus, prises côté serveur.since_idest le repère, et il compte plus ici que nulle part ailleurs : sans lui, l’appel revient immédiatement avec ce qui se trouvait déjà dans la boîte.- Un délai expiré est une réponse, pas une erreur
- Quand rien n’arrive, il renvoie
timed_outà vrai, avec la durée réellement attendue, et dit explicitement que rappeler est la façon de continuer à attendre. Deux ou trois appels forment une attente normale pour un email d’inscription : c’est la boucle, et ce sont trois tours au lieu de quatre-vingt-dix. - Il y a 8 places d’attente, et aucune file
- Quand elles sont toutes occupées, l’appel revient immédiatement et le dit, plutôt que de faire la queue derrière sept autres agents. C’est le bon type d’échec : un agent à qui l’on dit « trop d’attentes en cours » peut lister la boîte et continuer, tandis qu’un agent qui fait la queue ne peut que patienter.
La limite par adresse s’applique encore à l’intérieur : notre boucle est soumise à la même limite de débit que la vôtre, donc une attente côté serveur n’est pas un moyen de contourner le plancher, seulement un moyen d’arrêter de la payer en tours. Pour une suite de tests, rien de tout cela ne vaut la peine : un test est déjà un processus autorisé à faire une pause, et une boucle dans le langage où le test est écrit est bien plus facile à déboguer qu’une boucle distante. Le guide MCP couvre le reste des outils.
La boucle entière, une fois
Tout ce qui précède, dans un seul fichier : une échéance issue d’une horloge monotone, un intervalle qui s’élargit avec un aléa à sens unique, Retry-After respecté et non compté comme une tentative, un repère pour décider ce qui est nouveau, un filtre sur l’objet, et une requête supplémentaire pour récupérer le message que la liste ne fait qu’entrevoir.
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'}")C’est volontairement une cinquantaine de lignes de bibliothèque standard et un seul client HTTP. Il n’y a rien à installer, rien à configurer et aucun secret nulle part dedans — c’est bien le but : la même forme se déplace sans changement vers Node, vers un script shell, ou vers ce que votre framework de test utilise déjà pour faire des requêtes.
- Lisez le repère avant l’action qui provoque le courrier, jamais après.
- Demandez une première fois immédiatement, et ne faites une pause qu’ensuite. Ne faites jamais de pause en premier.
- Prenez l’échéance à partir d’une horloge monotone, et vérifiez-la au début de chaque passage.
- Gardez l’intervalle à une seconde ou plus par adresse, et n’ajoutez de l’aléa que vers le haut.
- Faites une pause exactement de ce que
Retry-Afterindique, et ne comptez pas un refus comme une tentative. - Branchez-vous sur le slug
error:unknown_domainetinvalid_addressveulent dire stop, pas attente. - Faites correspondre l’expéditeur ou l’objet, et arrêtez de parcourir la liste quand vous atteignez le repère.
- Ouvrez le message avant de l’analyser — la liste porte un aperçu, pas le corps.
- Échouez en donnant l’adresse, le filtre, le budget, et les objets que la boucle a rejetés.
Neuf règles, et huit d’entre elles existent à cause d’un échec que quelqu’un a dû reconstituer à partir d’un journal. Celle qui ne parle pas d’échec est la deuxième : demander une fois avant la première pause est ce qui fait qu’une attente pour un message déjà arrivé prend quatre millisecondes au lieu d’une seconde — ce qui, sur une suite de deux cents tests, représente trois minutes d’horloge murale que personne n’a ensuite à expliquer.
Questions
GrabMail propose-t-il un webhook ?
Non, et ce n’est pas un manque en attente d’être comblé. Le service reçoit du courrier et l’expose en HTTP sans clé : il n’y a pas de compte derrière une adresse publique auquel accrocher un callback, et pas de file d’attente pour conserver une livraison que votre point de terminaison aurait refusée. Si votre workflow ne peut vraiment pas interroger, la page de comparaison nomme les services qui en proposent un.
À quelle fréquence ai-je le droit d’interroger une adresse ?
Une fois par seconde, par adresse — et c’est le modèle prévu, pas sa limite extrême. Il n’y a ni quota journalier, ni quota mensuel, ni crédit de rafale à gérer. Vingt boîtes interrogées une fois par seconde depuis un seul exécuteur, c’est un usage ordinaire ; le seul autre plafond est de 1,200 requêtes par minute depuis une seule source, ce qui correspond exactement à ces vingt-là, pas à une vingt-et-unième.
Pourquoi ma boucle a-t-elle renvoyé un message d’une exécution de test précédente ?
Parce qu’elle a pris la première entrée de la liste sans demander quand elle était arrivée. Une boîte conserve tout ce qui lui a été envoyé pendant 5 jours, et une adresse réutilisée est pleine de l’exécution précédente. Lisez l’identifiant le plus récent avant de déclencher le courrier et ignorez tout ce qui se trouve à partir de cet identifiant vers le bas — ou supprimez le contenu de la boîte au début du test, ce qui coûte une requête par message et supprime complètement l’ambiguïté.
Une boîte vide est-elle un 404 ?
Non. Une boîte vide est délibérément un 200 avec count: 0 et une liste vide, pour qu’une boucle d’interrogation n’ait jamais à traiter « pas encore » comme un cas particulier. Un 404 venant du point de terminaison de liste signifie que le domaine n’est pas hébergé ici ; un 404 venant d’un seul message signifie que cet identifiant n’est pas dans cette boîte, ou qu’il a expiré.
Combien de temps dois-je attendre un email de vérification ?
Soixante à cent vingt secondes dans un test automatisé, trente à soixante pour une personne qui attend devant un écran. La plupart du courrier généré par une machine arrive en quelques secondes à un seul chiffre ; la longue traîne appartient à la file de l’expéditeur plutôt qu’à la livraison. Si c’est systématiquement proche de votre échéance, une échéance plus longue n’est pas la réponse — autre chose ne va pas.
Deux processus peuvent-ils interroger la même adresse à la fois ?
Oui. Lire ne consomme rien, donc les deux voient chaque message et aucun ne cache le courrier à l’autre. Ils partagent quand même le budget d’une requête par seconde pour cette adresse, donc deux boucles qui demandent chaque seconde se feront chacune refuser environ la moitié du temps : donnez-leur deux secondes chacune, ou laissez l’une faire l’interrogation et transmettre les résultats à l’autre.
Dois-je interroger, ou utiliser l’outil d’attente via MCP ?
Interrogez, si vous écrivez un test ou un script : un processus qui a le droit de faire une pause devrait la faire, et une boucle dans votre propre langage est plus facile à déboguer qu’une boucle distante. Utilisez wait_for_message quand l’appelant paie au tour plutôt qu’à la seconde, ce qui en pratique signifie un agent IA. Il attend jusqu’à 25 secondes par appel, filtre sur l’expéditeur et l’objet, et renvoie un simple délai expiré que vous pouvez rappeler tout simplement.


