← Retour au blog

Microsoft Graph API — Ce que la doc ne vous dit pas sur le rate limiting d'OneNote

Ensky Lin7 min read

Depuis un moment, je développe Note Bridge, un outil qui migre les carnets OneNote vers Notion. Au départ, je pensais que la conversion de contenu serait le plus gros défi — transformer correctement le HTML complexe d'OneNote en blocs Notion. En réalité, c'est la gestion de la limitation de débit de Microsoft qui m'a pris le plus de temps.

Ce n'est pas un article du genre « copie-colle ce snippet de backoff et c'est plié ». L'API Graph regorge de cas particuliers qu'une boucle de retry générique ne saura pas gérer — il faut tôt ou tard modéliser le système de limites au lieu de simplement réagir aux 429. Voici ce que j'en ai retenu.

1. Limitation de débit en 3 × 2 dimensions

La limitation de débit de OneNote n'est pas un simple plafond. Elle se décline en trois dimensions — par minute, par heure et requêtes simultanées — chacune appliquée à deux niveaux : par utilisateur et par application [1]. Le produit donne six limites distinctes à respecter ; sinon, les erreurs 429 arrivent immédiatement.

Conséquence : la stratégie classique de recul polynomial ne suffit pas. On peut être en dessous de la limite par minute tout en dépassant le plafond horaire. Ou un seul utilisateur ne pose aucun problème, mais le total de tous les utilisateurs dépasse le plafond global de l'application. Chaque dimension nécessite son propre suivi.

Cela dit, l'expérience montre que les limites réelles semblent plus généreuses que ce qu'indique la documentation. Nous codons néanmoins selon la spécification — si Microsoft décide un jour de l'appliquer strictement, nous ne voulons pas de mauvaises surprises.

2. Pas d'en-tête Retry-After

Une API bien conçue inclut un en-tête Retry-After dans ses réponses 429, pour indiquer au client quand il peut réessayer en toute sécurité. La raison n'est pas claire — peut-être la complexité du mécanisme — mais l'API OneNote de Graph ne fournit pas cet en-tête.

Sans Retry-After, les stratégies de recul simples ne fonctionnent pas. On peut allonger le délai après chaque 429, mais rien ne garantit que la tentative suivante ne sera pas rejetée elle aussi. Pour une application en production, c'est un problème sérieux. La seule solution fiable est d'implémenter un limiteur de débit conforme à la spécification de Microsoft — nous avons construit le nôtre avec Cloudflare Durable Objects, en suivant la consommation sur chaque dimension et en fournissant notre propre Retry-After utilisable par le frontend comme par le backend.

3. Les deux en-têtes que presque personne n'utilise

Une fois notre limiteur en place, on a découvert que Graph renvoie en fait deux en-têtes de réponse utiles — il est simplement facile de les manquer, car ils ne se trouvent pas sur le chemin des 429.

  • x-ms-throttle-limit-percentage — présent dans les réponses réussies. Il indique à quelle distance vous êtes d'une limite (0,0–1,0). Au-delà d'environ 0,8, c'est la zone jaune ; à 1,0, la requête suivante a de très fortes chances de renvoyer 429.
  • x-ms-throttle-scope — présent dans les réponses 429. Les valeurs sont User, Application, ou les deux. Il indique quel niveau a été franchi.

Ces deux en-têtes ont changé la conception. Le premier permet de déclencher un disjoncteur souple avant de recevoir un 429, au lieu d'attendre que le système nous prévienne. Le second permet de router le 429 vers le bon disjoncteur — si c'est un déclenchement au niveau application, ralentir un seul utilisateur ne sert à rien, il faut ralentir tout le monde. Si c'est au niveau utilisateur, les autres locataires peuvent continuer normalement.

Si vous ne réagissez qu'au code de statut 429, vous pilotez votre limiteur conforme à la spécification à moitié aveugle.

4. L'API de traitement par lots n'aide pas vraiment

Microsoft Graph propose un mécanisme de traitement par lots permettant de regrouper plusieurs requêtes en un seul appel HTTP. On s'attendrait à ce que cela compte comme une seule requête, mais ce n'est pas le cas : chaque requête interne du lot est décomptée individuellement contre les limites de débit [2].

Résultat : $batch n'a pratiquement aucun intérêt pour notre cas d'usage — tout au plus économise-t-on un peu de latence réseau.

Il y a aussi un piège plus subtil. Un POST $batch peut retourner HTTP 200 au niveau extérieur, alors que les sous-réponses internes renvoient 429, 502 ou 503. Lors d'une migration en production, nous avons silencieusement perdu deux sections parce que notre boucle de retry ne ré-essayait que le lot extérieur — les erreurs 5xx des sous-réponses passaient à la trappe. Si vous utilisez $batch, la logique de retry doit agir sur chaque sous-réponse, pas sur l'enveloppe.

5. La spirale de la mort

L'incident le plus douloureux que nous ayons connu ne venait pas des 429 eux-mêmes, mais de ce qui s'est produit après les 429. Un utilisateur a lancé une migration de 1 300 pages. Les 200 premières pages sont passées sans souci. Puis un 429. Notre consumer l'a rattrapé et a demandé à Cloudflare Queues un retry 30 secondes plus tard. Jusque-là raisonnable.

Sauf que les 1 100 messages restants étaient déjà tous en file ; passées les 30 secondes, ils se sont réveillés à peu près en même temps, sont retombés en masse sur la limite et ont tous récolté un 429. Chaque nouvelle vague de 429 prolongeait la fenêtre de throttling. La courbe dessinait une jolie boucle auto-entretenue : limitation → retries → limitation → encore plus de retries. Pendant une demi-heure, pas une seule page n'a avancé.

Deux corrections ont fini par tout changer :

  1. Un vrai disjoncteur au niveau du limiteur, pas à chaque point d'appel. Quand l'amont renvoie des 429 de manière soutenue, le limiteur lui-même déclenche et tous les appelants reçoivent un 429 local pendant une fenêtre de cooldown. Cela fait converger N boucles de retry indépendantes vers une seule attente partagée, au lieu d'avoir N appelants effectuant chacun 30 retries × 5 minutes.
  2. Séparation premier plan / arrière-plan. Note Bridge a deux types de charges qui sollicitent Graph : les scans interactifs de carnet (un utilisateur attend) et les migrations en arrière-plan (personne ne regarde l'écran). Si une migration de 1 300 pages monopolise tout le budget de débit, l'utilisateur qui essaie de scanner son carnet attend éternellement. Notre parade : deux « files » de limiteur, la file arrière-plan plafonnée à environ 50 % du budget. Le premier plan garde toujours de la marge.

6. Le bug subtil de retryAfterMs

Celui-ci mérite une mention à part, car il nous a pris du temps à repérer. Quand le limiteur vérifie deux couches (per-user et per-app) et que les deux sont saturées, quel retryAfterMs doit-il renvoyer à l'appelant ?

On avait Math.min(userRetry, appRetry) — retourner la plus courte attente, ça sonne aimable. C'était faux. Si le niveau user dit « attends 2 secondes » et le niveau app dit « attends 30 secondes », réessayer après 2 secondes te vaut immédiatement un autre 429. La bonne réponse est Math.max(userRetry, appRetry) — la requête n'est sûre que lorsque les deux couches ont de la capacité. Une correction d'un seul caractère qui a éliminé toute une catégorie de retries fantômes.

Conclusion

La plupart des tutoriels de rate limiting s'arrêtent à « réessaye avec un backoff exponentiel et du jitter ». Pour Microsoft Graph, c'est insuffisant. Il faut :

  • Modéliser la matrice 3 × 2 au lieu de deviner.
  • Lire x-ms-throttle-limit-percentage et déclencher un disjoncteur souple avant le 429.
  • Router les 429 via x-ms-throttle-scope pour qu'un déclenchement utilisateur ne pénalise pas toute l'app (et vice versa).
  • Réessayer au niveau des sous-réponses de $batch, pas seulement de l'enveloppe.
  • Utiliser un disjoncteur partagé, sinon vos tempêtes de retries survivront à la fenêtre de throttling réelle.
  • Séparer premier plan et arrière-plan pour que l'UX interactive ne soit pas affamée.

L'absence de Retry-After dans Microsoft Graph est le grief le plus visible, mais la leçon plus profonde est que la limitation de débit est un système, pas un en-tête. Construisez-la comme tel.

Note Bridge migre les carnets OneNote vers Notion.

Prêt à migrer vos notes ?

Essayez Note Bridge gratuitement — migrez jusqu'à 20 pages sans carte bancaire.

Commencer gratuitement