Changer le thème

Cloudflare D1 en pratique : SQLite en edge avec réplication mondiale

Easton editorial illustration: large SQLite database core on an abstract edge-map desk

0,01 milliseconde.

C’est le temps pour lire une ligne SQLite en local. La même requête sur Cloudflare D1 prend environ 0,5 ms ; PostgreSQL en accès inter-région peut atteindre 1 à 3 ms — la différence paraît mince ? Mais si vos utilisateurs sont à Tokyo et la base en Virginie, le seul aller-retour réseau dépasse déjà 100 ms.

L’an dernier, sur un projet déployé mondialement, ce problème m’a bloqué. Les bases classiques imposent soit une latence élevée, soit un read/write split complexe. Puis Cloudflare a annoncé la réplication mondiale de D1 lors du Developer Week 2025 — et les choses ont commencé à changer.

Cet article explique comment D1 déplace SQLite vers l’edge, ce que signifient concrètement Durable Objects, horodatages Lamport et l’API Sessions, et quand choisir D1 — ou passer son chemin.

I. Qu’est-ce que D1 : SQLite sur le réseau edge

En bref, D1 est SQLite hébergé sur le réseau edge de Cloudflare, lisible et inscriptible depuis plus de 300 villes dans le monde.

Si vous pensez que c’est juste « SQLite + CDN », vous sous-estimez l’ambition du produit. SQLite classique a des limites en production : fichier unique difficile à distribuer, pas de reprise intégrée, écritures qui verrouillent toute la base. D1 repense ces contraintes.

En quoi diffère-t-il de SQLite traditionnel

Intégration. D1 tourne directement dans Cloudflare Workers ; vous interrogez la base comme une fonction :

// wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx"

// Requête dans le Worker
export default {
  async fetch(request, env) {
    const { results } = await env.DB.prepare(
      "SELECT * FROM users WHERE id = ?"
    ).bind(1).all();
    return Response.json(results);
  }
}

Time Travel. D1 conserve automatiquement l’historique ; vous pouvez revenir à n’importe quel instant — un luxe pour SQLite. Compte gratuit : 30 jours ; comptes payants : plus long.

Réplication mondiale (mise à jour majeure 2025). Le nœud primary est dans une région ; les réplicas de lecture se synchronisent partout. Un utilisateur à Singapour lit depuis le réplica local — latence passée de trois chiffres à quelques millisecondes.

Limites à connaître avant de choisir

D1 n’est pas universel :

10 Go maximum par base. Au-delà : sharding ou autre solution. Jusqu’à 50 000 bases par compte — suffisant pour la plupart des projets ; si votre modèle est « une base par utilisateur », calculez bien.

Architecture à écrivain unique. Un seul nœud traite les écritures à la fois ; débit plafonné autour de 500-2 000 writes/sec, loin des 10K-50K de PostgreSQL. Enchères temps réel, pipelines de logs : D1 peut ne pas tenir.

Cohérence séquentielle, pas forte. Vous venez d’écrire : la lecture suivante peut ne pas voir la donnée — sauf si vous utilisez correctement l’API Sessions.

En pratique, D1 convient surtout aux applications Web lecture-intensives. La plupart des sites font plus de 90 % de lectures ; la réplication mondiale répond depuis le nœud le plus proche — gain tangible.

II. Architecture D1 : Durable Objects et réplication mondiale

Ce chapitre est plus technique, mais indispensable pour bien utiliser D1.

Durable Objects : un « majordome » par base

Le cœur de D1, ce sont les Durable Objects. Imaginez un processus dédié par base, qui :

  1. Garantit l’unicité globale : toutes les écritures passent par lui — pas de conflits concurrents sur la même ligne
  2. Maintient le journal des transactions : chaque écriture est enregistrée pour reprise et synchronisation des réplicas
  3. Coordonne les réplicas : indique aux copies mondiales quand mettre à jour

Design astucieux. Les bases distribuées classiques coordonnent plusieurs nœuds — latence et pannes compliquent tout. D1 : un primary, file d’attente pour les écritures, traitement séquentiel, puis sync asynchrone vers les réplicas.

Snapshot Isolation : les lectures ne bloquent pas

Un SELECT sur D1 ne fait pas la queue au primary : il lit un « instantané » sur le réplica le plus proche.

Exemple : primary à Pékin, réplicas à Tokyo, Singapour, Sydney. Un utilisateur à Tokyo lit depuis Tokyo — état figé au début de la requête. Même si le primary écrit en parallèle, votre lecture n’est pas bloquée.

Problème : vous écrivez puis lisez tout de suite — la donnée peut manquer, le réplica n’étant pas encore à jour.

D’où l’API Sessions.

Horodatages Lamport : donner un ordre au chaos

Leslie Lamport (1978) a proposé un ordonnancement logique des événements en système distribué — horodatages Lamport. Chaque événement a une horloge logique ; un événement ultérieur a un timestamp plus grand.

D1 s’en sert pour la cohérence séquentielle : dans une session, si vous écrivez puis lisez, D1 garantit de voir l’état post-écriture, pas un vieux réplica.

Après chaque écriture, D1 renvoie un « marque-page » (commit token) : « toutes les modifications jusqu’ici sont visibles ». À la requête suivante, avec ce token, D1 garantit un état au moins aussi récent.

Utilisateur → écrit commande → obtient commit token « abc123 »
Utilisateur → lit commande (token « abc123 ») → voit la donnée fraîche

Comment fonctionne la réplication mondiale

À la création, D1 choisit une « région primary » — par défaut le datacenter Cloudflare le plus proche, ou manuellement.

Écriture :

  1. Requête vers le nœud edge le plus proche
  2. Routage vers le Durable Object de la région primary
  3. Écriture sur le fichier primary
  4. Réplication asynchrone vers les réplicas mondiaux

Lecture :

  1. Requête vers le nœud edge le plus proche
  2. Lecture depuis le réplica local
  3. Avec session : application du marque-page de cohérence

Cloudflare annonce que la réplication mondiale n’est pas facturée en plus — le transfert est inclus. Les écritures restent routées vers le primary : latence selon la distance. Primary aux États-Unis, utilisateurs en Asie : latence d’écriture perceptible.

III. API Sessions en pratique : cohérence séquentielle en code

Passons à la pratique.

L’API Sessions (2025) résout le problème « écrire puis lire ». Si vous connaissez la cohérence causale de MongoDB ou les follower reads de CockroachDB, le principe est proche — un marqueur trace la causalité.

Usage minimal

// Créer une Session
const session = env.DB.withSession();

// Lecture routée vers le réplica proche
const { results } = await session.prepare(
  "SELECT * FROM products WHERE category = ?"
).bind("electronics").all();

// Écriture routée automatiquement vers le primary
await session.prepare(
  "INSERT INTO orders (user_id, product_id, quantity) VALUES (?, ?, ?)"
).bind(userId, productId, 2).run();

// Marque-page de cohérence de la session
const bookmark = session.latestCommitToken;

La clé : withSession(). Elle crée un contexte où toutes les opérations partagent la même vue de cohérence.

Trois modes de cohérence

1. first-unconstrained (défaut)

const session = env.DB.withSession("first-unconstrained");

Mode le plus permissif. Lecture depuis le réplica proche, même s’il n’est pas à jour. Listes produits, articles de blog — faible exigence de fraîcheur.

2. first-primary

const session = env.DB.withSession("first-primary");

La première lecture va au primary ; les suivantes aux réplicas. Vous voyez au moins l’état au moment de la création de session — utile pour voir « ce qu’on vient d’écrire » sans interroger le primary à chaque fois.

3. Reprendre une session avec un marque-page

// Récupérer le marque-page précédent depuis l'en-tête
const previousToken = request.headers.get("x-d1-token") ?? "first-unconstrained";

// Créer une session qui continue la précédente
const session = env.DB.withSession(previousToken);

// Opérations...

// Renvoyer le nouveau marque-page
response.headers.set("x-d1-token", session.latestCommitToken);

Usage le plus puissant. Stockez le marque-page côté client (Cookie, en-tête) pour la cohérence inter-requêtes.

Cas concret : e-commerce

Plateforme mondiale : navigation produits depuis le réplica proche (latence minimale) ; après commande, l’utilisateur doit voir sa commande.

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Token depuis l'en-tête (null à la première requête)
    const token = request.headers.get("x-d1-token") ?? "first-unconstrained";
    const session = env.DB.withSession(token);

    // Cas 1 : liste produits (cohérence forte non requise)
    if (url.pathname === "/api/products") {
      const { results } = await session.prepare(
        "SELECT * FROM products WHERE status = ?"
      ).bind("active").all();

      return new Response(JSON.stringify(results), {
        headers: {
          "Content-Type": "application/json",
          "x-d1-token": session.latestCommitToken
        }
      });
    }

    // Cas 2 : créer commande (écriture → primary)
    if (url.pathname === "/api/orders" && request.method === "POST") {
      const body = await request.json();

      await session.prepare(`
        INSERT INTO orders (user_id, total_amount, status)
        VALUES (?, ?, ?)
      `).bind(body.userId, body.total, "pending").run();

      // Lire immédiatement pour confirmer l'écriture
      const order = await session.prepare(`
        SELECT * FROM orders WHERE user_id = ?
        ORDER BY created_at DESC LIMIT 1
      `).bind(body.userId).first();

      return new Response(JSON.stringify(order), {
        headers: {
          "Content-Type": "application/json",
          "x-d1-token": session.latestCommitToken
        }
      });
    }

    // Cas 3 : détail commande (token garantit la cohérence)
    if (url.pathname.startsWith("/api/orders/")) {
      const orderId = url.pathname.split("/")[3];

      const order = await session.prepare(
        "SELECT * FROM orders WHERE id = ?"
      ).bind(orderId).first();

      return new Response(JSON.stringify(order), {
        headers: {
          "Content-Type": "application/json",
          "x-d1-token": session.latestCommitToken
        }
      });
    }
  }
}

Navigation : first-unconstrained, performance optimale. Après commande, le client conserve le token pour les requêtes suivantes.

Côté client

Stocker x-d1-token et l’envoyer à chaque requête :

// Exemple frontend
let d1Token = localStorage.getItem('d1-token') ?? 'first-unconstrained';

async function fetchProducts() {
  const response = await fetch('/api/products', {
    headers: { 'x-d1-token': d1Token }
  });
  d1Token = response.headers.get('x-d1-token');
  localStorage.setItem('d1-token', d1Token);
  return response.json();
}

async function createOrder(data) {
  const response = await fetch('/api/orders', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-d1-token': d1Token
    },
    body: JSON.stringify(data)
  });
  d1Token = response.headers.get('x-d1-token');
  localStorage.setItem('d1-token', d1Token);
  return response.json();
}

Peu de code pour un gros problème. Sans ce mécanisme, après une commande la liste peut rester vide — mauvaise expérience.

IV. Benchmarks et comparaison concurrentielle

Les chiffres ne mentent pas. Comparaison des principales options, d’après docs officielles et retours communautaires.

Latence

SolutionLatence lecture (p50)Latence lecture (p99)Latence écriture (p50)Notes
D1~0,5 ms~2-5 ms~5-30 msLecture réplica edge, écriture primary
Turso~0,02 ms~0,1 ms~15-50 msLecture embarquée, très rapide
PlanetScale~3-8 ms~10-20 ms~3-8 msMySQL compatible, proxy lecture/écriture
PostgreSQL (Neon)~3-10 ms~20-50 ms~1-5 msArchitecture classique, cold start
0,5 ms
Latence lecture D1
p50, réplica edge
0,02 ms
Latence lecture Turso
lecture embarquée
500-2K
Débit écriture D1
writes/sec
10 Go
Limite par base
sharding au-delà
Source: Documentation officielle et benchmarks communautaires

Turso en lecture : 0,02 ms, quasi accès mémoire locale — SQLite embarqué copié sur le nœud edge. Contrepartie : sync plus complexe, latence d’écriture plus élevée.

D1 : 0,5 ms en lecture, excellent pour l’edge. Écriture : routage vers le primary — distance physique = plancher de latence. Primary côte ouest US, utilisateur à Singapour : 30 ms minimum.

PlanetScale et Neon : mieux pour les apps classiques. Latence moins spectaculaire, mais écosystème mature. Procédures stockées, triggers, index riches : plus adaptés.

Débit

SolutionDébit lecture (QPS)Débit écriture (QPS)Notes
D110K-100K500-2KLimite par base
TursoIllimité (lecture locale)Limité par syncChaque nœud edge lit en local
PlanetScale10K-50K5K-20KExtensible par sharding
PostgreSQL10K-100K10K-50KSelon taille d’instance

Point faible de D1 : écriture. Écrivain unique = plafond. Plus de 5 000 écritures/seconde : goulot ou sharding (complexité), ou autre solution.

Quotas gratuits

SolutionStockageLectureÉcritureRemarques
D15 Go25 milliards lignes/mois50 millions lignes/moisMax 10 Go/base
Turso9 Go1 milliard lignes/mois25 millions lignes/moisInclut trafic réplication
PlanetScale1 Go10 milliards lignes/mois10 milliards lignes/moisPas de limite d’écriture
Neon0,5 Go100 millions unités/mois100 millions unités/moisunité = lecture ou écriture

D1 est généreux en lecture (25 milliards de lignes). Attention à l’écriture : 50 millions/mois, ~1,66 million/jour — logs et tracking dépassent vite.

Tarification

D1 : à l’usage, sans minimum. Au-delà du gratuit : 0,001 $/million lignes lues, 0,10 $/million lignes écrites ; stockage 0,75 $/Go/mois.

Turso : « lignes lues » et « trafic réplication » — coût de réplication élevé si mises à jour fréquentes.

PlanetScale : lecture et écriture ; écriture moins chère que D1, lecture un peu plus.

Conseil : écosystème Cloudflare (Workers, KV, R2) → facturation D1 plus claire. Projet isolé : testez les trois avec vos données réelles.

V. Arbre de décision : quand choisir D1

Scénarios adaptés

Application lecture-intensive : site de contenu, e-commerce (navigation), blog, documentation. Plus de 90 % de lectures — réplication mondiale, latence en millisecondes.

Utilisateurs mondiaux : base mono-région = latence inter-océans. D1 rapproche les données de l’utilisateur.

Déjà sur Cloudflare Workers : intégration native, quelques lignes de config, pas de pool de connexions, pas de cold start DB.

Volume sous 10 Go : au-delà, sharding. Modèle « une base par locataire » : la limite importe moins.

Scénarios inadaptés

Écritures fréquentes : enchères, logs, IoT — dizaines de milliers d’écritures/seconde. PostgreSQL, ClickHouse, TimescaleDB.

Transactions complexes : niveau SQLite actuel ; pas SERIALIZABLE, pas de transactions cross-base, pas de procédures stockées avancées.

Plus de 10 Go : sharding possible mais lourd. Données volumineuses (séries temporelles, archives) : autre choix dès le départ.

Cohérence forte : D1 est en cohérence finale — lecture immédiate après écriture peut échouer sans Sessions API.

Migration depuis PostgreSQL

1. Différences de dialecte

SQLite vs PostgreSQL :

  • Pas de clause RETURNING (insert puis select)
  • Pas de type SERIAL (INTEGER PRIMARY KEY AUTOINCREMENT)
  • Pas de JSONB (TEXT + json_extract())
  • Pas de type ARRAY (table associée)

2. Outils de migration

# Exporter PostgreSQL
pg_dump --format=insert mydb > dump.sql

# Importer dans D1
npx wrangler d1 execute my-d1-database --file=dump.sql

Schémas complexes : ajustements manuels.

3. Mode de connexion

Longues connexions → appels stateless. ORM à adapter ou SQL natif. Prisma a un adaptateur D1, encore en évolution.

Décision rapide

Écritures > 1 000/seconde ?
├─ Oui → pas D1
└─ Non
    └─ Cohérence forte requise ?
        ├─ Oui → pas D1 (ou Sessions API)
        └─ Non
            └─ Volume > 10 Go ?
                ├─ Oui → à évaluer prudemment
                └─ Non → D1 convient bien

Conclusion

D1 en trois phrases : déploiement edge pour latence en millisecondes, serverless sans ops DB, Sessions API pour la cohérence post-écriture sans complexité excessive.

Ce n’est pas la solution universelle. Écritures fréquentes, transactions complexes, très gros volumes : PostgreSQL et bases temporelles restent pertinents. Pas de balle en argent — des compromis.

Application Web globale, lecture-intensive, déjà sur Workers : D1 mérite un essai. Créer une base de test prend quelques minutes :

# Créer la base
npx wrangler d1 create my-first-db

# Créer une table
npx wrangler d1 execute my-first-db --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)"

# Insérer des données
npx wrangler d1 execute my-first-db --command="INSERT INTO users (name) VALUES ('test')"

Testez la latence Tokyo ↔ base côte ouest — vous saurez si D1 correspond à votre projet.


"D1 est la base SQLite edge de Cloudflare, avec réplication mondiale en lecture et expérience serverless. L’API Sessions utilise les horodatages Lamport pour la cohérence séquentielle et résout le problème classique « écrire puis lire » en système distribué."

Références

Démarrer rapidement avec Cloudflare D1

Du création de la base à la lecture cohérente via l'API Sessions

⏱️ Estimated time: 15 min

  1. 1

    Step 1: Créer une base D1

    Utilisez wrangler CLI pour créer la base :

    ```bash
    npx wrangler d1 create my-first-db
    ```

    Après création, vous obtenez un database_id à configurer dans wrangler.toml :

    ```toml
    [[d1_databases]]
    binding = "DB"
    database_name = "my-first-db"
    database_id = "your-database-id"
    ```
  2. 2

    Step 2: Créer les tables

    Exécutez du SQL pour créer le schéma :

    ```bash
    npx wrangler d1 execute my-first-db --command="CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)"
    ```

    Vous pouvez aussi exécuter un fichier SQL en lot :

    ```bash
    npx wrangler d1 execute my-first-db --file=./schema.sql
    ```
  3. 3

    Step 3: Utiliser l'API Sessions dans un Worker

    Créez une connexion avec session pour garantir la cohérence après écriture :

    ```typescript
    export default {
    async fetch(request, env) {
    // Récupérer le session token depuis l'en-tête
    const token = request.headers.get("x-d1-token") ?? "first-unconstrained";
    const session = env.DB.withSession(token);

    // Écrire des données
    await session.prepare("INSERT INTO users (name) VALUES (?)")
    .bind("test").run();

    // Lire en garantissant la cohérence
    const { results } = await session.prepare("SELECT * FROM users")
    .all();

    return new Response(JSON.stringify(results), {
    headers: { "x-d1-token": session.latestCommitToken }
    });
    }
    }
    ```
  4. 4

    Step 4: Configurer la réplication mondiale en lecture

    Spécifiez la région primaire dans wrangler.toml :

    ```toml
    [[d1_databases]]
    binding = "DB"
    database_name = "my-first-db"
    database_id = "your-database-id"
    primary_location_hint = "apne1" # Région Tokyo
    ```

    Codes de région disponibles :
    - apne1 : Tokyo
    - sfo1 : San Francisco
    - eur3 : Francfort

FAQ

Quelle différence entre Cloudflare D1 et Turso ?
Les deux sont des bases SQLite edge, mais l'architecture diffère :

• D1 : écrivain unique, écritures routées vers le primary, latence de lecture ~0,5 ms — idéal pour lecture-intensive
• Turso : lecture embarquée, latence plus basse (~0,02 ms), mais synchronisation plus complexe

D1 s'intègre nativement à Cloudflare Workers avec un quota gratuit plus généreux (25 milliards de lignes lues/mois) ; Turso excelle en latence de lecture pour les cas ultra sensibles.
Comment contourner la limite de 10 Go par base D1 ?
Trois approches :

• Sharding par module métier : une base par module
• Isolation par locataire : une base par tenant (jusqu'à 50 000 bases)
• Stockage hybride : données chaudes dans D1, froides vers R2 ou autre stockage objet

Si le volume dépasse durablement 10 Go, évaluez PlanetScale ou PostgreSQL classique.
Comment choisir entre les trois modes de l'API Sessions ?
Selon le scénario :

• first-unconstrained (défaut) : listes produits, blogs — faible exigence de fraîcheur
• first-primary : voir les données « juste écrites » sans interroger le primary à chaque fois
• mode commit token : e-commerce, commandes — cohérence inter-requêtes

Recommandation e-commerce : first-unconstrained à la navigation, sauvegarder le token après commande, le renvoyer ensuite.
D1 convient-il aux écritures fréquentes ?
Pas vraiment. L'architecture à écrivain unique plafonne autour de 500-2 000 writes/sec, loin des 10K-50K de PostgreSQL.

Si votre application ressemble à :
• enchères temps réel
• pipelines de logs, tracking
• collecte IoT
• plus de 1 000 écritures/seconde

Préférez PostgreSQL, ClickHouse ou TimescaleDB.
Migrer de PostgreSQL vers D1 : points d'attention ?
Principales différences :

• Dialecte SQL : SQLite sans RETURNING, SERIAL, JSONB, ARRAY
• Connexion : appels stateless au lieu de connexions longues
• ORM : adaptateur Prisma pour D1, encore en évolution

Étapes :
1. Exporter avec pg_dump
2. Adapter manuellement le SQL incompatible
3. Importer via wrangler d1 execute

Testez d'abord sur un petit volume avant migration complète.
La réplication mondiale D1 est-elle facturée en plus ?
Non. Cloudflare indique explicitement que la réplication mondiale en lecture n'est pas facturée séparément ; le coût de transfert est inclus.

À noter :
• Les écritures restent routées vers le primary — latence selon la distance physique
• Primary aux États-Unis, utilisateurs en Asie : latence d'écriture ~30 ms+
• Quota lecture très élevé (25 milliards de lignes/mois), écriture 50 millions de lignes/mois

Placez le primary là où se concentrent vos utilisateurs.

11 min de lecture · Publié le: 5 mai 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog