{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases","title":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","description":"Una guía práctica para correr SQLite como base de datos distribuida en el edge. Internos de replicación de LiteFS, extensiones libSQL, arquitectura de Turso, y benchmarks reales mostrando cuándo SQLite destruye a Postgres para workloads read-heavy en el edge.","language":"es","datePublished":"2025-10-30","dateModified":null,"contentFormat":"text/markdown","content":"Databases\n\n# SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta\n\n2025-10-309 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/sqlite-litefs-distributed-edge-databases>)\n\n## Por Qué SQLite en el Edge Tiene Sentido\n\nLa idea fundamental es esta: **si tu base de datos es un archivo en la misma máquina que tu aplicación, las lecturas son una llamada a función, no un roundtrip de red.** La diferencia de latencia es extraordinaria:\n\n```\nPostgres (misma región): ~2ms por query\nPostgres (cross-region): ~50-180ms por query\nSQLite (archivo local, modo WAL): ~0.02ms por query\n```\n\nNo es un typo. Las lecturas de SQLite en modo WAL son literalmente 100x más rápidas que Postgres en la misma región porque no hay TCP handshake, no hay negociación de protocolo, no hay parsing de query por el cable, no hay serialización de resultados. Es un lookup en B-tree sobre un archivo memory-mapped.\n\nLa trampa, obviamente, es que SQLite es una base de datos single-node. No replica. No clusteriza. Escribe a un solo archivo.\n\nAhí es donde entran LiteFS, Turso y libSQL.\n\n## LiteFS: Replicación Transparente para SQLite\n\n[LiteFS](<https://fly.io/docs/litefs/>), construido por el equipo de [Fly.io](<https://fly.io/>), es genuinamente ingenioso. Es un filesystem FUSE que se sienta entre tu aplicación y el archivo SQLite real, interceptando todas las operaciones de escritura.\n\nAsí funciona la replicación a bajo nivel:\n\n```\nLa aplicación escribe a SQLite\n ↓\nFUSE intercepta la syscall de escritura\n ↓\nLiteFS captura las páginas WAL cambiadas\n ↓\nLas páginas se envían al nodo primary via HTTP\n ↓\nEl primary aplica las páginas y las reenvía a las réplicas\n ↓\nLas réplicas aplican las páginas a su archivo SQLite local\n```\n\nLa decisión de diseño clave es que **LiteFS replica a nivel de página, no a nivel de SQL.** No re-ejecuta sentencias SQL, envía las páginas de base de datos de 4KB realmente modificadas. Esto significa:\n\n1. La replicación es SQL-agnóstica (funciona con cualquier extensión de SQLite, funciones custom, etc.)\n2. El overhead es proporcional a los datos cambiados, no a la complejidad del query\n3. No hay necesidad de parsear o validar SQL del lado de la réplica\n\n### Configurando LiteFS en Fly.io\n\n```yaml\n# fly.toml\n[mounts]\n source = \"litefs\"\n destination = \"/var/lib/litefs\"\n\n[processes]\n app = \"/usr/local/bin/litefs mount\"\n\n# litefs.yml\nfuse:\n dir: \"/litefs\"\n\ndata:\n dir: \"/var/lib/litefs\"\n\nproxy:\n addr: \":8080\"\n target: \"localhost:3000\"\n db: \"myapp.db\"\n passthrough:\n - \"*.ico\"\n - \"*.png\"\n\nexec:\n - cmd: \"node server.js\"\n\nlease:\n type: \"consul\"\n advertise-url: \"http://${FLY_ALLOC_ID}.vm.${FLY_APP_NAME}.internal:20202\"\n consul:\n url: \"${FLY_CONSUL_URL}\"\n key: \"litefs/${FLY_APP_NAME}\"\n```\n\nLa sección ` proxy ` es importante, LiteFS corre un reverse proxy que automáticamente reenvía las requests de escritura al nodo primary. Tu aplicación no necesita saber sobre la topología de replicación.\n\n### Write Forwarding\n\nCuando tu app corriendo en una réplica en Tokyo recibe un POST request que necesita escribir a la base de datos, pasa esto:\n\n```\n1. El request llega al proxy LiteFS en la réplica de Tokyo\n2. LiteFS detecta que es una escritura (POST/PUT/DELETE o SQL INSERT/UPDATE/DELETE)\n3. LiteFS reenvía el request al primary (Virginia) via header Fly-Replay\n4. El primary ejecuta la escritura\n5. Las páginas cambiadas replican a Tokyo (~120ms después)\n6. La respuesta vuelve al cliente\n```\n\nLa latencia de escritura es el roundtrip de red al primary más el tiempo de replicación. Para un usuario en Tokyo escribiendo a un primary en Virginia, son aproximadamente 180-250ms. Las lecturas, sin embargo, siguen pegando a la réplica local: 0.02ms.\n\nPara un sitio de contenido donde el 98% de las requests son lecturas, este trade-off es excepcionalmente favorable.\n\n## Turso y libSQL: SQLite con Capacidades de Servidor\n\n[Turso](<https://turso.tech/>) toma un enfoque diferente. En vez de replicación transparente a nivel de filesystem, forkearon SQLite en libSQL y le agregaron features a nivel de servidor:\n\n```\nAdiciones de libSQL sobre SQLite:\n├── Modo servidor (sqld), escucha en HTTP/WebSocket\n├── Embedded replicas, archivo SQLite local que sincroniza con remoto\n├── Namespaces, aislamiento de base de datos multi-tenant\n├── Búsqueda vectorial, embeddings vectoriales nativos (tipo VECTOR)\n├── Mejoras en ALTER TABLE, cambios de tipo de columna\n└── Funciones definidas por usuario basadas en WASM\n```\n\nLa feature de embedded replica es lo que hace a Turso interesante para deployments en el edge:\n\n```javascript\nimport { createClient } from \"@libsql/client\";\n\nconst client = createClient({\n url: \"file:local-replica.db\", // archivo SQLite local\n syncUrl: \"libsql://mydb-myorg.turso.io\", // primary remoto\n authToken: process.env.TURSO_AUTH_TOKEN,\n syncInterval: 60, // sincronizar cada 60 segundos\n});\n\n// Las lecturas pegan al archivo local, ~0.1ms\nconst users = await client.execute(\"SELECT * FROM users WHERE active = 1\");\n\n// Las escrituras van al primary remoto, ~50ms\nawait client.execute({\n sql: \"INSERT INTO users (name, email) VALUES (?, ?)\",\n args: [\"Alice\", \"alice@example.com\"],\n});\n\n// Forzar sync cuando se necesita datos frescos\nawait client.sync();\n```\n\nEl mecanismo de sync es más sofisticado que LiteFS. Turso trackea un vector clock por namespace y solo envía el delta de frames cambiados desde el último sync. La embedded replica mantiene un WAL local que se reconcilia durante el sync.\n\n## Benchmarks Reales\n\nCorrí estos contra un dataset del mundo real: 2 millones de registros de productos, ~800MB de base de datos, corriendo en máquinas de Fly.io (shared-cpu-1x, 256MB RAM).\n\n### Performance de Lectura\n\n```\nQuery: SELECT * FROM products WHERE category = ? AND price < ? ORDER BY rating DESC LIMIT 20\nIndex: CREATE INDEX idx_products_cat_price ON products(category, price)\n\nSQLite (local, modo WAL):\n p50: 0.08ms\n p99: 0.3ms\n Throughput: 42,000 queries/sec (single connection)\n\nTurso (embedded replica, sincronizado):\n p50: 0.09ms\n p99: 0.35ms\n Throughput: 38,000 queries/sec\n\nPostgres (misma región, pgbouncer):\n p50: 1.8ms\n p99: 4.2ms\n Throughput: 8,500 queries/sec (50 conexiones)\n\nPostgres (cross-region, réplica más cercana):\n p50: 12ms\n p99: 45ms\n Throughput: 2,100 queries/sec\n```\n\nSQLite es 5x más rápido que Postgres en la misma región y 150x más rápido que cross-region. Los números de throughput son todavía más dramáticos porque SQLite no tiene overhead de conexión, cada query es una llamada a función directa al B-tree.\n\n### Performance de Escritura\n\n```\nQuery: INSERT INTO orders (user_id, product_id, quantity, total) VALUES (?, ?, ?, ?)\n\nSQLite (local, modo WAL):\n p50: 0.05ms\n p99: 2.1ms (spikes de checkpoint WAL)\n Throughput: 18,000 inserts/sec (single writer)\n\nLiteFS (escritura al primary, replicar):\n p50: 85ms (red al primary)\n p99: 210ms\n Throughput: ~200 writes/sec (limitado por RTT de red)\n\nTurso (embedded replica, escritura al primary):\n p50: 52ms\n p99: 130ms\n Throughput: ~350 writes/sec\n\nPostgres (misma región):\n p50: 1.5ms\n p99: 5ms\n Throughput: 12,000 inserts/sec (escritores paralelos)\n```\n\nAcá es donde Postgres gana de manera decisiva. La restricción de single-writer de SQLite significa que no puede paralelizar escrituras. Y en un deployment edge, cada escritura tiene que pegar al primary por la red. Si se está haciendo más de unos cientos de escrituras por segundo, se necesita Postgres (u otra base de datos multi-writer).\n\n## Cuándo Usar Esto Realmente\n\nDespués de correr SQLite en el edge en producción por más de un año, este es mi framework de decisión:\n\n**Utilizar SQLite + LiteFS/Turso cuando:**\n\n- El ratio lectura-a-escritura es 90%+ lecturas\n- El dataset entra cómodamente en una sola máquina (\\<100GB)\n- Necesitás latencia de lectura sub-milisegundo globalmente\n- Querés cero overhead operacional de base de datos\n- Estás en una plataforma que lo soporta (Fly.io, Cloudflare Workers con D1, etc.)\n\n**Quedarse con Postgres cuando:**\n\n- El throughput de escritura importa (\\>500 writes/sec sostenidos)\n- Necesitás transacciones complejas entre múltiples tablas\n- El dataset es grande (\\>100GB) o crece rápido\n- Necesitás features avanzados (JSONB, full-text con diccionarios custom, vistas materializadas)\n- Teners un equipo DBA dedicado\n\n**El enfoque híbrido** (lo que realmente hago para la mayoría de los proyectos):\n\n```\n┌─────────────────────────────┐\n│ Edge (regiones Fly.io) │\n│ ┌───────────────────────┐ │\n│ │ SQLite (via LiteFS) │ │\n│ │ - Sesiones de usuario │ │\n│ │ - Cache de contenido │ │\n│ │ - Feature flags │ │\n│ │ - Queries read-heavy │ │\n│ └───────────────────────┘ │\n└─────────────┬───────────────┘\n │ escrituras + queries complejos\n ▼\n┌─────────────────────────────┐\n│ Central (región única) │\n│ ┌───────────────────────┐ │\n│ │ Postgres │ │\n│ │ - Datos transaccion. │ │\n│ │ - Analytics │ │\n│ │ - Full-text search │ │\n│ │ - Tablas write-heavy │ │\n│ └───────────────────────┘ │\n└─────────────────────────────┘\n```\n\nSQLite en el edge para el hot path read-heavy. Postgres en una región central para las cosas transaccionales y write-heavy. Las bases SQLite del edge sincronizan desde Postgres via un pipeline CDC o ETL periódico.\n\n## Avanzado: Extensiones Custom de libSQL\n\nUna cosa que me empujó hacia Turso fueron las funciones definidas por usuario basadas en WASM de libSQL. Puede escribir funciones custom en cualquier lenguaje que compile a WASM y cargarlas en SQLite:\n\n```rust\n// distance.rs, compilado a WASM\nuse libsql_bindgen::*;\n\n#[libsql_bindgen]\npub fn haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {\n let r = 6371.0; // Radio de la Tierra en km\n let dlat = (lat2 - lat1).to_radians();\n let dlon = (lon2 - lon1).to_radians();\n let a = (dlat / 2.0).sin().powi(2)\n + lat1.to_radians().cos() * lat2.to_radians().cos()\n * (dlon / 2.0).sin().powi(2);\n let c = 2.0 * a.sqrt().asin();\n r * c\n}\n```\n\nDespués usalo en SQL:\n\n```sql\nSELECT name, haversine(lat, lon, -34.6037, -58.3816) AS distance_km\nFROM stores\nWHERE distance_km < 10\nORDER BY distance_km;\n```\n\nEsto corre a velocidad nativa dentro del motor de queries de SQLite. Sin llamadas a servicios externos, sin overhead de UDF de lenguajes interpretados. Para queries geo en bases de datos edge, esto representa un avance significativo.\n\n## Realidad Operacional\n\nSeamos honestos sobre las asperezas. SQLite en el edge presenta desafíos:\n\n1. **Las migraciones de schema son complicadas.** No es posible correr ` ALTER TABLE ... ADD COLUMN ` con un valor default en una tabla grande sin lockearla. libSQL mejora esto, pero el ALTER TABLE de SQLite vanilla es notoriamente limitado.\n2. **El monitoreo es inmaduro.** No hay equivalente de ` pg_stat_statements `. Teners que instrumentar tu código de aplicación o usar el callback ` sqlite3_trace_v2 ` de SQLite.\n3. **Los backups requieren pensar.** No es posible simplemente hacer ` pg_dump `. LiteFS maneja backup continuo a S3, pero la recuperación point-in-time requiere mantener archivos WAL.\n4. **El connection pooling no existe** (porque no hay conexiones). Esto en realidad es una feature, no más pesadillas de configuración de PgBouncer, pero significa que tu aplicación debe manejar correctamente el busy timeout de SQLite.\n\nA pesar de estos temas, estoy genuinamente convencido de que SQLite en el edge es la arquitectura correcta para una gran clase de aplicaciones que actualmente están over-provisionando con Postgres o MySQL. Las características de performance son demasiado buenas para ignorar, y el tooling finalmente maduró al punto de estar production-ready.\n\nEl mundo de las bases de datos está teniendo un momento de \"la herramienta correcta para el trabajo correcto\". Postgres es excepcional, pero no todo necesita una base de datos cliente-servidor. A veces la mejor base de datos es la que está embebida en tu aplicación, replicada a cada nodo edge, sirviendo lecturas en microsegundos.\n\n## Preguntas frecuentes\n\n### ¿SQLite puede manejar escrituras concurrentes en producción?\n\nSQLite soporta un escritor a la vez, incluso en modo WAL. Para workloads read-heavy (95%+ lecturas), esto raramente es un cuello de botella. Para workloads write-heavy, se necesita serializar escrituras a través de un solo proceso o usar Turso/libSQL que implementa coordinación de escrituras server-side.\n\n### ¿Cómo funciona la replicación de LiteFS?\n\nLiteFS usa un filesystem FUSE que intercepta las escrituras WAL de SQLite. Cuando una transacción commitea, LiteFS captura las páginas cambiadas y las envía a los nodos réplica por HTTP. Las réplicas aplican las páginas a su copia local. El lag de replicación es típicamente 50-200ms dependiendo de la distancia de red.\n\n### ¿Cuándo debería usar SQLite en vez de Postgres en el edge?\n\nUsar SQLite cuando tu workload es read-heavy (\\>90% lecturas), tus datos entran en una sola máquina (menos de ~100GB), se necesita latencia de lectura sub-milisegundo, y se desea cero overhead operacional. Utilizar Postgres cuando necesités transacciones complejas, full-text search con diccionarios custom, o workloads write-heavy.\n\n## Tips\n\n1. **WAL mode es innegociable:** Siempre ejecutar ` PRAGMA journal_mode=WAL; ` antes de cualquier otra cosa. WAL (Write-Ahead Logging) permite lectores concurrentes durante escrituras. Sin esto, una sola escritura lockea toda la base de datos. LiteFS requiere modo WAL, no va a replicar correctamente con rollback journals.\n2. **Truco de latencia con embedded replicas de Turso:** Utilizar ` @libsql/client ` con ` syncUrl ` y ` syncInterval ` para crear una réplica SQLite local que sincroniza con la red edge de Turso. Las lecturas pegan al archivo local (~0.1ms), las escrituras van al primary (~50ms). Configurar ` syncInterval: 1000 ` para casi-tiempo-real con ` client.sync() ` para freshness on-demand.\n3. **Consistencia de lectura basada en TXID de LiteFS:** LiteFS expone el ID de transacción actual en el endpoint ` /litefs/.primary `. Después de una escritura, capturar el TXID y pasarlo via header ` Fly-Replay: state=txid_value `. La réplica va a bloquear hasta haber replicado hasta ese TXID, dándote consistencia read-your-writes sin rutear todas las lecturas al primary.\n4. **Tuning del page size de SQLite:** El page size default es 4096 bytes. Para workloads pesados con BLOBs grandes, incrementar a ` PRAGMA page_size=65536; ` antes de crear la base de datos (es inmutable después). Para OLTP de filas chicas, 4096 es óptimo. LiteFS replica a granularidad de página, así que páginas más grandes significan más datos por cambio replicado.\n\n- sqlite\n- litefs\n- turso\n- libsql\n- edge-computing\n- distributed-databases\n- fly-io\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#title"},{"level":2,"text":"Por Qué SQLite en el Edge Tiene Sentido","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#por-qu-sqlite-en-el-edge-tiene-sentido"},{"level":2,"text":"LiteFS: Replicación Transparente para SQLite","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#litefs-replicacin-transparente-para-sqlite"},{"level":3,"text":"Configurando LiteFS en Fly.io","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#configurando-litefs-en-flyio"},{"level":3,"text":"Write Forwarding","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#write-forwarding"},{"level":2,"text":"Turso y libSQL: SQLite con Capacidades de Servidor","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#turso-y-libsql-sqlite-con-capacidades-de-servidor"},{"level":2,"text":"Benchmarks Reales","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#benchmarks-reales"},{"level":3,"text":"Performance de Lectura","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#performance-de-lectura"},{"level":3,"text":"Performance de Escritura","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#performance-de-escritura"},{"level":2,"text":"Cuándo Usar Esto Realmente","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#cundo-usar-esto-realmente"},{"level":2,"text":"Avanzado: Extensiones Custom de libSQL","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#avanzado-extensiones-custom-de-libsql"},{"level":2,"text":"Realidad Operacional","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#realidad-operacional"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#questions"},{"level":3,"text":"¿SQLite puede manejar escrituras concurrentes en producción?","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-1"},{"level":3,"text":"¿Cómo funciona la replicación de LiteFS?","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-2"},{"level":3,"text":"¿Cuándo debería usar SQLite en vez de Postgres en el edge?","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#article-tips"}],"sections":[{"heading":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","level":1,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases","content":"Databases"},{"heading":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","level":1,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#title","content":"2025-10-309 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/sqlite-litefs-distributed-edge-databases>)"},{"heading":"Por Qué SQLite en el Edge Tiene Sentido","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#por-qu-sqlite-en-el-edge-tiene-sentido","content":"La idea fundamental es esta: **si tu base de datos es un archivo en la misma máquina que tu aplicación, las lecturas son una llamada a función, no un roundtrip de red.** La diferencia de latencia es extraordinaria:\n\n```\nPostgres (misma región): ~2ms por query\nPostgres (cross-region): ~50-180ms por query\nSQLite (archivo local, modo WAL): ~0.02ms por query\n```\n\nNo es un typo. Las lecturas de SQLite en modo WAL son literalmente 100x más rápidas que Postgres en la misma región porque no hay TCP handshake, no hay negociación de protocolo, no hay parsing de query por el cable, no hay serialización de resultados. Es un lookup en B-tree sobre un archivo memory-mapped.\n\nLa trampa, obviamente, es que SQLite es una base de datos single-node. No replica. No clusteriza. Escribe a un solo archivo.\n\nAhí es donde entran LiteFS, Turso y libSQL."},{"heading":"LiteFS: Replicación Transparente para SQLite","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#litefs-replicacin-transparente-para-sqlite","content":"[LiteFS](<https://fly.io/docs/litefs/>), construido por el equipo de [Fly.io](<https://fly.io/>), es genuinamente ingenioso. Es un filesystem FUSE que se sienta entre tu aplicación y el archivo SQLite real, interceptando todas las operaciones de escritura.\n\nAsí funciona la replicación a bajo nivel:\n\n```\nLa aplicación escribe a SQLite\n ↓\nFUSE intercepta la syscall de escritura\n ↓\nLiteFS captura las páginas WAL cambiadas\n ↓\nLas páginas se envían al nodo primary via HTTP\n ↓\nEl primary aplica las páginas y las reenvía a las réplicas\n ↓\nLas réplicas aplican las páginas a su archivo SQLite local\n```\n\nLa decisión de diseño clave es que **LiteFS replica a nivel de página, no a nivel de SQL.** No re-ejecuta sentencias SQL, envía las páginas de base de datos de 4KB realmente modificadas. Esto significa:\n\n1. La replicación es SQL-agnóstica (funciona con cualquier extensión de SQLite, funciones custom, etc.)\n2. El overhead es proporcional a los datos cambiados, no a la complejidad del query\n3. No hay necesidad de parsear o validar SQL del lado de la réplica"},{"heading":"Configurando LiteFS en Fly.io","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#configurando-litefs-en-flyio","content":"```yaml\n# fly.toml\n[mounts]\n source = \"litefs\"\n destination = \"/var/lib/litefs\"\n\n[processes]\n app = \"/usr/local/bin/litefs mount\"\n\n# litefs.yml\nfuse:\n dir: \"/litefs\"\n\ndata:\n dir: \"/var/lib/litefs\"\n\nproxy:\n addr: \":8080\"\n target: \"localhost:3000\"\n db: \"myapp.db\"\n passthrough:\n - \"*.ico\"\n - \"*.png\"\n\nexec:\n - cmd: \"node server.js\"\n\nlease:\n type: \"consul\"\n advertise-url: \"http://${FLY_ALLOC_ID}.vm.${FLY_APP_NAME}.internal:20202\"\n consul:\n url: \"${FLY_CONSUL_URL}\"\n key: \"litefs/${FLY_APP_NAME}\"\n```\n\nLa sección ` proxy ` es importante, LiteFS corre un reverse proxy que automáticamente reenvía las requests de escritura al nodo primary. Tu aplicación no necesita saber sobre la topología de replicación."},{"heading":"Write Forwarding","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#write-forwarding","content":"Cuando tu app corriendo en una réplica en Tokyo recibe un POST request que necesita escribir a la base de datos, pasa esto:\n\n```\n1. El request llega al proxy LiteFS en la réplica de Tokyo\n2. LiteFS detecta que es una escritura (POST/PUT/DELETE o SQL INSERT/UPDATE/DELETE)\n3. LiteFS reenvía el request al primary (Virginia) via header Fly-Replay\n4. El primary ejecuta la escritura\n5. Las páginas cambiadas replican a Tokyo (~120ms después)\n6. La respuesta vuelve al cliente\n```\n\nLa latencia de escritura es el roundtrip de red al primary más el tiempo de replicación. Para un usuario en Tokyo escribiendo a un primary en Virginia, son aproximadamente 180-250ms. Las lecturas, sin embargo, siguen pegando a la réplica local: 0.02ms.\n\nPara un sitio de contenido donde el 98% de las requests son lecturas, este trade-off es excepcionalmente favorable."},{"heading":"Turso y libSQL: SQLite con Capacidades de Servidor","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#turso-y-libsql-sqlite-con-capacidades-de-servidor","content":"[Turso](<https://turso.tech/>) toma un enfoque diferente. En vez de replicación transparente a nivel de filesystem, forkearon SQLite en libSQL y le agregaron features a nivel de servidor:\n\n```\nAdiciones de libSQL sobre SQLite:\n├── Modo servidor (sqld), escucha en HTTP/WebSocket\n├── Embedded replicas, archivo SQLite local que sincroniza con remoto\n├── Namespaces, aislamiento de base de datos multi-tenant\n├── Búsqueda vectorial, embeddings vectoriales nativos (tipo VECTOR)\n├── Mejoras en ALTER TABLE, cambios de tipo de columna\n└── Funciones definidas por usuario basadas en WASM\n```\n\nLa feature de embedded replica es lo que hace a Turso interesante para deployments en el edge:\n\n```javascript\nimport { createClient } from \"@libsql/client\";\n\nconst client = createClient({\n url: \"file:local-replica.db\", // archivo SQLite local\n syncUrl: \"libsql://mydb-myorg.turso.io\", // primary remoto\n authToken: process.env.TURSO_AUTH_TOKEN,\n syncInterval: 60, // sincronizar cada 60 segundos\n});\n\n// Las lecturas pegan al archivo local, ~0.1ms\nconst users = await client.execute(\"SELECT * FROM users WHERE active = 1\");\n\n// Las escrituras van al primary remoto, ~50ms\nawait client.execute({\n sql: \"INSERT INTO users (name, email) VALUES (?, ?)\",\n args: [\"Alice\", \"alice@example.com\"],\n});\n\n// Forzar sync cuando se necesita datos frescos\nawait client.sync();\n```\n\nEl mecanismo de sync es más sofisticado que LiteFS. Turso trackea un vector clock por namespace y solo envía el delta de frames cambiados desde el último sync. La embedded replica mantiene un WAL local que se reconcilia durante el sync."},{"heading":"Benchmarks Reales","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#benchmarks-reales","content":"Corrí estos contra un dataset del mundo real: 2 millones de registros de productos, ~800MB de base de datos, corriendo en máquinas de Fly.io (shared-cpu-1x, 256MB RAM)."},{"heading":"Performance de Lectura","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#performance-de-lectura","content":"```\nQuery: SELECT * FROM products WHERE category = ? AND price < ? ORDER BY rating DESC LIMIT 20\nIndex: CREATE INDEX idx_products_cat_price ON products(category, price)\n\nSQLite (local, modo WAL):\n p50: 0.08ms\n p99: 0.3ms\n Throughput: 42,000 queries/sec (single connection)\n\nTurso (embedded replica, sincronizado):\n p50: 0.09ms\n p99: 0.35ms\n Throughput: 38,000 queries/sec\n\nPostgres (misma región, pgbouncer):\n p50: 1.8ms\n p99: 4.2ms\n Throughput: 8,500 queries/sec (50 conexiones)\n\nPostgres (cross-region, réplica más cercana):\n p50: 12ms\n p99: 45ms\n Throughput: 2,100 queries/sec\n```\n\nSQLite es 5x más rápido que Postgres en la misma región y 150x más rápido que cross-region. Los números de throughput son todavía más dramáticos porque SQLite no tiene overhead de conexión, cada query es una llamada a función directa al B-tree."},{"heading":"Performance de Escritura","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#performance-de-escritura","content":"```\nQuery: INSERT INTO orders (user_id, product_id, quantity, total) VALUES (?, ?, ?, ?)\n\nSQLite (local, modo WAL):\n p50: 0.05ms\n p99: 2.1ms (spikes de checkpoint WAL)\n Throughput: 18,000 inserts/sec (single writer)\n\nLiteFS (escritura al primary, replicar):\n p50: 85ms (red al primary)\n p99: 210ms\n Throughput: ~200 writes/sec (limitado por RTT de red)\n\nTurso (embedded replica, escritura al primary):\n p50: 52ms\n p99: 130ms\n Throughput: ~350 writes/sec\n\nPostgres (misma región):\n p50: 1.5ms\n p99: 5ms\n Throughput: 12,000 inserts/sec (escritores paralelos)\n```\n\nAcá es donde Postgres gana de manera decisiva. La restricción de single-writer de SQLite significa que no puede paralelizar escrituras. Y en un deployment edge, cada escritura tiene que pegar al primary por la red. Si se está haciendo más de unos cientos de escrituras por segundo, se necesita Postgres (u otra base de datos multi-writer)."},{"heading":"Cuándo Usar Esto Realmente","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#cundo-usar-esto-realmente","content":"Después de correr SQLite en el edge en producción por más de un año, este es mi framework de decisión:\n\n**Utilizar SQLite + LiteFS/Turso cuando:**\n\n- El ratio lectura-a-escritura es 90%+ lecturas\n- El dataset entra cómodamente en una sola máquina (\\<100GB)\n- Necesitás latencia de lectura sub-milisegundo globalmente\n- Querés cero overhead operacional de base de datos\n- Estás en una plataforma que lo soporta (Fly.io, Cloudflare Workers con D1, etc.)\n\n**Quedarse con Postgres cuando:**\n\n- El throughput de escritura importa (\\>500 writes/sec sostenidos)\n- Necesitás transacciones complejas entre múltiples tablas\n- El dataset es grande (\\>100GB) o crece rápido\n- Necesitás features avanzados (JSONB, full-text con diccionarios custom, vistas materializadas)\n- Teners un equipo DBA dedicado\n\n**El enfoque híbrido** (lo que realmente hago para la mayoría de los proyectos):\n\n```\n┌─────────────────────────────┐\n│ Edge (regiones Fly.io) │\n│ ┌───────────────────────┐ │\n│ │ SQLite (via LiteFS) │ │\n│ │ - Sesiones de usuario │ │\n│ │ - Cache de contenido │ │\n│ │ - Feature flags │ │\n│ │ - Queries read-heavy │ │\n│ └───────────────────────┘ │\n└─────────────┬───────────────┘\n │ escrituras + queries complejos\n ▼\n┌─────────────────────────────┐\n│ Central (región única) │\n│ ┌───────────────────────┐ │\n│ │ Postgres │ │\n│ │ - Datos transaccion. │ │\n│ │ - Analytics │ │\n│ │ - Full-text search │ │\n│ │ - Tablas write-heavy │ │\n│ └───────────────────────┘ │\n└─────────────────────────────┘\n```\n\nSQLite en el edge para el hot path read-heavy. Postgres en una región central para las cosas transaccionales y write-heavy. Las bases SQLite del edge sincronizan desde Postgres via un pipeline CDC o ETL periódico."},{"heading":"Avanzado: Extensiones Custom de libSQL","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#avanzado-extensiones-custom-de-libsql","content":"Una cosa que me empujó hacia Turso fueron las funciones definidas por usuario basadas en WASM de libSQL. Puede escribir funciones custom en cualquier lenguaje que compile a WASM y cargarlas en SQLite:\n\n```rust\n// distance.rs, compilado a WASM\nuse libsql_bindgen::*;\n\n#[libsql_bindgen]\npub fn haversine(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {\n let r = 6371.0; // Radio de la Tierra en km\n let dlat = (lat2 - lat1).to_radians();\n let dlon = (lon2 - lon1).to_radians();\n let a = (dlat / 2.0).sin().powi(2)\n + lat1.to_radians().cos() * lat2.to_radians().cos()\n * (dlon / 2.0).sin().powi(2);\n let c = 2.0 * a.sqrt().asin();\n r * c\n}\n```\n\nDespués usalo en SQL:\n\n```sql\nSELECT name, haversine(lat, lon, -34.6037, -58.3816) AS distance_km\nFROM stores\nWHERE distance_km < 10\nORDER BY distance_km;\n```\n\nEsto corre a velocidad nativa dentro del motor de queries de SQLite. Sin llamadas a servicios externos, sin overhead de UDF de lenguajes interpretados. Para queries geo en bases de datos edge, esto representa un avance significativo."},{"heading":"Realidad Operacional","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#realidad-operacional","content":"Seamos honestos sobre las asperezas. SQLite en el edge presenta desafíos:\n\n1. **Las migraciones de schema son complicadas.** No es posible correr ` ALTER TABLE ... ADD COLUMN ` con un valor default en una tabla grande sin lockearla. libSQL mejora esto, pero el ALTER TABLE de SQLite vanilla es notoriamente limitado.\n2. **El monitoreo es inmaduro.** No hay equivalente de ` pg_stat_statements `. Teners que instrumentar tu código de aplicación o usar el callback ` sqlite3_trace_v2 ` de SQLite.\n3. **Los backups requieren pensar.** No es posible simplemente hacer ` pg_dump `. LiteFS maneja backup continuo a S3, pero la recuperación point-in-time requiere mantener archivos WAL.\n4. **El connection pooling no existe** (porque no hay conexiones). Esto en realidad es una feature, no más pesadillas de configuración de PgBouncer, pero significa que tu aplicación debe manejar correctamente el busy timeout de SQLite.\n\nA pesar de estos temas, estoy genuinamente convencido de que SQLite en el edge es la arquitectura correcta para una gran clase de aplicaciones que actualmente están over-provisionando con Postgres o MySQL. Las características de performance son demasiado buenas para ignorar, y el tooling finalmente maduró al punto de estar production-ready.\n\nEl mundo de las bases de datos está teniendo un momento de \"la herramienta correcta para el trabajo correcto\". Postgres es excepcional, pero no todo necesita una base de datos cliente-servidor. A veces la mejor base de datos es la que está embebida en tu aplicación, replicada a cada nodo edge, sirviendo lecturas en microsegundos."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#questions","content":""},{"heading":"¿SQLite puede manejar escrituras concurrentes en producción?","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-1","content":"SQLite soporta un escritor a la vez, incluso en modo WAL. Para workloads read-heavy (95%+ lecturas), esto raramente es un cuello de botella. Para workloads write-heavy, se necesita serializar escrituras a través de un solo proceso o usar Turso/libSQL que implementa coordinación de escrituras server-side."},{"heading":"¿Cómo funciona la replicación de LiteFS?","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-2","content":"LiteFS usa un filesystem FUSE que intercepta las escrituras WAL de SQLite. Cuando una transacción commitea, LiteFS captura las páginas cambiadas y las envía a los nodos réplica por HTTP. Las réplicas aplican las páginas a su copia local. El lag de replicación es típicamente 50-200ms dependiendo de la distancia de red."},{"heading":"¿Cuándo debería usar SQLite en vez de Postgres en el edge?","level":3,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#question-3","content":"Usar SQLite cuando tu workload es read-heavy (\\>90% lecturas), tus datos entran en una sola máquina (menos de ~100GB), se necesita latencia de lectura sub-milisegundo, y se desea cero overhead operacional. Utilizar Postgres cuando necesités transacciones complejas, full-text search con diccionarios custom, o workloads write-heavy."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#article-tips","content":"1. **WAL mode es innegociable:** Siempre ejecutar ` PRAGMA journal_mode=WAL; ` antes de cualquier otra cosa. WAL (Write-Ahead Logging) permite lectores concurrentes durante escrituras. Sin esto, una sola escritura lockea toda la base de datos. LiteFS requiere modo WAL, no va a replicar correctamente con rollback journals.\n2. **Truco de latencia con embedded replicas de Turso:** Utilizar ` @libsql/client ` con ` syncUrl ` y ` syncInterval ` para crear una réplica SQLite local que sincroniza con la red edge de Turso. Las lecturas pegan al archivo local (~0.1ms), las escrituras van al primary (~50ms). Configurar ` syncInterval: 1000 ` para casi-tiempo-real con ` client.sync() ` para freshness on-demand.\n3. **Consistencia de lectura basada en TXID de LiteFS:** LiteFS expone el ID de transacción actual en el endpoint ` /litefs/.primary `. Después de una escritura, capturar el TXID y pasarlo via header ` Fly-Replay: state=txid_value `. La réplica va a bloquear hasta haber replicado hasta ese TXID, dándote consistencia read-your-writes sin rutear todas las lecturas al primary.\n4. **Tuning del page size de SQLite:** El page size default es 4096 bytes. Para workloads pesados con BLOBs grandes, incrementar a ` PRAGMA page_size=65536; ` antes de crear la base de datos (es inmutable después). Para OLTP de filas chicas, 4096 es óptimo. LiteFS replica a granularidad de página, así que páginas más grandes significan más datos por cambio replicado.\n\n- sqlite\n- litefs\n- turso\n- libsql\n- edge-computing\n- distributed-databases\n- fly-io\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/sqlite-litefs-distributed-edge-databases"},{"text":"LiteFS","url":"https://fly.io/docs/litefs/"},{"text":"Fly.io","url":"https://fly.io/"},{"text":"Turso","url":"https://turso.tech/"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases"},"headline":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","description":"Una guía práctica para correr SQLite como base de datos distribuida en el edge. Internos de replicación de LiteFS, extensiones libSQL, arquitectura de Turso, y benchmarks reales mostrando cuándo SQLite destruye a Postgres para workloads read-heavy en el edge.","datePublished":"2025-10-30","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Databases","keywords":"sqlite, litefs, turso, libsql, edge-computing, distributed-databases, fly-io","wordCount":1720,"timeRequired":"PT9M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/sqlite-litefs-distributed-edge-databases"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"SQLite en el Edge: LiteFS, Turso, y Por Qué Postgres No Siempre Es la Respuesta","item":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿SQLite puede manejar escrituras concurrentes en producción?","acceptedAnswer":{"@type":"Answer","text":"SQLite soporta un escritor a la vez, incluso en modo WAL. Para workloads read-heavy (95%+ lecturas), esto raramente es un cuello de botella. Para workloads write-heavy, se necesita serializar escrituras a través de un solo proceso o usar Turso/libSQL que implementa coordinación de escrituras server-side."}},{"@type":"Question","name":"¿Cómo funciona la replicación de LiteFS?","acceptedAnswer":{"@type":"Answer","text":"LiteFS usa un filesystem FUSE que intercepta las escrituras WAL de SQLite. Cuando una transacción commitea, LiteFS captura las páginas cambiadas y las envía a los nodos réplica por HTTP. Las réplicas aplican las páginas a su copia local. El lag de replicación es típicamente 50-200ms dependiendo de la distancia de red."}},{"@type":"Question","name":"¿Cuándo debería usar SQLite en vez de Postgres en el edge?","acceptedAnswer":{"@type":"Answer","text":"Usar SQLite cuando tu workload es read-heavy (>90% lecturas), tus datos entran en una sola máquina (menos de ~100GB), se necesita latencia de lectura sub-milisegundo, y se desea cero overhead operacional. Utilizar Postgres cuando necesités transacciones complejas, full-text search con diccionarios custom, o workloads write-heavy."}}]}]}],"contentHash":"424092e000ea2de4bf54f720d7086d0899a31f967e57889ec86a8cff4414f726","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/sqlite-litefs-distributed-edge-databases","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":70170,"contentBytes":15469}}
