La proposition de valeur de Workers devient claire lorsque vous voyez ce qui est possible dans un seul gestionnaire : récupérer un enregistrement de D1 avec SQL, vérifier une entrée dans KV, récupérer un objet de R2, appeler un service interne via Service Binding et mettre en file d'attente le travail asynchrone dans une file d'attente - le tout dans le même appel, chaque liaison étant injectée en tant que propriété de env et disponible avec une seule ligne de code. Pas de SDK séparé à instancier, pas de configuration réseau à gérer, pas d'informations d'identification flottant dans les variables d'environnement. wrangler.toml déclare les liaisons, le runtime les délivre. Le problème est que chacune de ces opérations consomme une sous-requête du budget d'invocation, et la facture apparaît plus tôt qu'il n'y paraît.
Le modèle de liaisons et ce qu'il résout
Les liaisons sont la manière dont le runtime Workers connecte son code aux fonctionnalités de la plate-forme sans exposer les informations d'identification ou la configuration réseau. Lorsque vous déclarez [[d1_databases]] dans wrangler.toml avec un database_id, le runtime injecte un objet avec l'interface D1 dans env.DB. Lorsque vous déclarez [[kv_namespaces]] avec un id, le runtime injecte l'interface KV dans env.CACHE. Il n'y a pas de jeton d'authentification, pas d'URL de point de terminaison, pas de SDK tiers — la liaison est un appel direct à l'infrastructure Cloudflare.
Cela a des conséquences sur la sécurité et le fonctionnement. Un Worker compromis n'a pas d'informations d'identification à exfiltrer : il ne peut utiliser que les liaisons qui ont été déclarées pour lui, avec les autorisations dont disposent ces liaisons. Pour un Worker qui a uniquement besoin de lire à partir du KV, vous déclarez la liaison en lecture seule et le Worker ne peut littéralement pas écrire, indépendamment de ce que le code essaie de faire. Cette séparation des capacités est plus forte qu’une vérification des autorisations dans le code.
Pour la préparation et la production, chaque environnement possède ses propres ID de liaison dans wrangler.toml. Le code ne change pas — env.DB reste env.DB — mais lors de la mise en scène, il pointe vers une banque D1 différente, un espace de noms KV différent, un compartiment R2 différent. Il n'y a aucun risque que le code intermédiaire touche les données de production car les liaisons sont physiquement séparées.
Le budget de la sous-demande et comment il est consommé
Le plan gratuit comporte 50 sous-requêtes par appel. Le forfait payant en compte 1000. Chaque opération qui quitte l'isolat compte : fetch() pour toute URL, env.KV.get(), env.KV.put(), env.DB.prepare().run(), env.BUCKET.get(), env.QUEUE.send(), env.SERVICE.fetch(). Un appel à env.DB.batch() avec 5 requêtes compte comme 1 sous-requête — ce détail est critique.
Un gestionnaire typique d'une API qui renvoie un profil utilisateur enrichi : recherche l'utilisateur dans D1 par ID (1), recherche les préférences dans KV (2), vérifie une photo de profil dans R2 (3), appelle un service d'autorisation via Service Binding (4). Total : 4 sous-demandes dans la voie heureuse. S'adapte à 1 000 utilisateurs simultanés et reste dans la limite : 4 sous-requêtes par appel ne posent aucun problème.
Le problème se pose avec les boucles. Un gestionnaire qui traite une liste d’éléments et effectue une requête D1 par élément – le classique N+1 – explose rapidement. Cinquante éléments comportant chacun une requête atteignent la limite du forfait gratuit dès le premier appel. Avec 200 éléments sur le forfait payant, il se situe toujours dans les 1 000, mais la latence accumulée de 200 requêtes D1 séquentielles sera de plusieurs centaines de millisecondes. L'erreur lorsque la limite est atteinte arrive sous la forme d'une erreur réseau dans la sous-requête qui a dépassé — pas de message clair dans le corps de la réponse au client, juste une exception dans la queue.
Optimisations concrètes par liaison
Pour D1, db.batch() est l’optimisation la plus importante. Au lieu d'exécuter prepare().run() en séquence pour plusieurs requêtes, vous transmettez un tableau d'instructions à batch() et recevez un tableau de résultats — pour un coût total d'une sous-requête. Pour les requêtes qui ne dépendent pas les unes des autres (recherche de configurations de types différents par exemple), le batching élimine à la fois la latence séquentielle et les dépenses en sous-requêtes.
Les requêtes parallèles D1 — Promise.all([db.query1, db.query2]) — comptent toujours comme des sous-requêtes distinctes, mais elles s'exécutent en parallèle et la latence est déterminée par la plus lente et non par la somme. Utilisez Promise.all() lorsque les résultats sont indépendants et que vous n'avez pas besoin du lot pour une autre raison. Utilisez batch() lorsque vous souhaitez consolider le coût de la sous-demande.
Pour KV, le modèle le plus précieux est le cache du module. KV a une latence réseau (quelques millisecondes, même au mieux) et les données de configuration rarement modifiées n'ont pas besoin d'être relues à chaque appel. Un module peut déclarer une variable en portée globale :
let cachedConfig = null; export default { async fetch(request, env) { if (!cachedConfig) { cachedConfig = JSON.parse(await env.CONFIG.get('app-config')); } // usa cachedConfig } }
Tant que l'isolat réside dans le PoP, les invocations ultérieures réutilisent la valeur sans gaspiller une sous-requête. Le risque est l'obsolescence : si la configuration change, l'isolat mis en cache utilise toujours l'ancienne version jusqu'à sa suppression. Pour les données qui doivent être en temps quasi réel, KV avec un faible cacheTtl ou une relecture d'invocation est plus appropriée. Pour les indicateurs de fonctionnalités et les configurations d'infrastructure qui changent rarement, le cache de module est efficace.
Pour R2, la liaison prend en charge les requêtes de plage — env.BUCKET.get(key, { range: { offset, length } }) —, ce qui vous permet de récupérer uniquement la partie d'un objet dont vous avez besoin au lieu du fichier entier. Pour les fichiers volumineux pour lesquels vous avez besoin d'un en-tête, de métadonnées intégrées ou des premières lignes d'un CSV, la demande de plage permet d'économiser de la mémoire et du temps de transfert. La réponse est un ReadableStream qui peut être transmis directement au client sans mise en mémoire tampon.
Composition de plusieurs liaisons dans une même requête
Le véritable pouvoir vient lorsque vous avez besoin de plusieurs types de données pour construire une réponse. Une API de produit qui renvoie une page de recherche : requête SQL dans D1 pour les identifiants qui correspondent au filtre, lecture parallèle en KV pour les prix (qui changent fréquemment et vivent en KV pour les performances de lecture) et URL pré-signée de R2 pour l'image principale de chaque produit.
Cette composition fonctionne naturellement — env.DB, env.PRICES, env.ASSETS sont tous disponibles dans le même gestionnaire. La conception qui ne fonctionne pas bien consiste à le faire en séquence pour chaque élément d'une liste. La bonne version : la requête D1 renvoie 20 ID, Promise.all() pour les 20 KV lectures en parallèle (20 sous-requêtes, mais en parallèle), Promise.all() pour les 20 URL R2 (20 sous-requêtes supplémentaires). Total : 41 sous-requêtes (1 D1 + 20 KV + 20 R2), dans la limite payante de 1 000, avec une latence déterminée par la plus lente des 40 récupérations parallèles, et non par la somme de 41.
Surveillance de l'utilisation des sous-requêtes
Le runtime n’expose pas le nombre de sous-requêtes par appel directement dans la queue. L'approche consiste à instrumenter : créer un simple wrapper qui incrémente un compteur à chaque opération de liaison et enregistre le total à la fin du gestionnaire. Exporté via Logpush, ce numéro permet de détecter quand un déploiement a augmenté la consommation — avant d'atteindre un cas limite dépassant la limite en production.
Un gestionnaire qui utilise 500 sous-requêtes alors qu’il pourrait en utiliser 50 avec un traitement par lots et un parallélisme corrects gaspille inutilement de la latence et du budget. Repenser ce gestionnaire après un incident de production est plus coûteux que de mesurer la consommation dès le début et de la corriger alors que N est encore faible.
A lire aussi
- KV vs R2 vs API Cache : quand utiliser chaque niveau de stockage Cloudflare -Cloudflare Workers en production : qu'est-ce qui change après hello world
- Workers : débogage, journaux et Workers Tail — observabilité à la périphérie sans serveur de journaux
- Workers : limites de CPU et de mémoire — que la documentation n'explique pas bien
- Testing Workers : unité, intégration et comment simuler le runtime sans dépendre de Cloudflare -Cloudflare KV : Que signifie une distribution mondiale lorsque vous devez écrire
