Herman Primo.
Volver al inicio

Blog

Todas las publicaciones técnicas.

Explora contenido sobre backend, APIs, bases de datos, seguridad, rendimiento y arquitectura de software.

12 publicación(es) encontrada(s)

Página 1 de 1

¿Por qué las computadoras empiezan a contar desde 0 y no desde 1?
Curiosidad de programación

¿Por qué las computadoras empiezan a contar desde 0 y no desde 1?

Carrusel: ¿Por qué tantos lenguajes empiezan a contar desde 0? Slide 1 — Hook 🤔 Tienes una lista con 5 elementos: ["A", "B", "C", "D", "E"] Pero sus posiciones son: 0 1 2 3 4 Espera... ¿5 elementos pero el último está en la posición 4? 😵‍💫 Si nosotros contamos: 1, 2, 3, 4, 5 👉 ¿por qué en tantos lenguajes de programación todo empieza desde 0 ? Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 2 — Deja de pensar en “número” En lugar de preguntar: ❌ “¿Qué número de elemento es?” prueba con: 📍 “¿Qué tan lejos está del inicio?” [A] [B] [C] [D] [E] ↑ INICIO Para llegar a A : ➡️ avanzas 0 posiciones. Para llegar a B : ➡️ avanzas 1 . Para llegar a C : ➡️ avanzas 2 . 💡 De repente, el cero empieza a tener sentido. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 3 — Ahora llévalo a memoria Supongamos que: 📦 Cada elemento ocupa 4 bytes 📍 El arreglo comienza en 1000 Podemos pensar en: dirección = inicio + índice × tamaño Entonces: array[0] → 1000 + 0×4 = 1000 array[1] → 1000 + 1×4 = 1004 array[2] → 1000 + 2×4 = 1008 👉 El índice 0 apunta naturalmente al inicio porque su desplazamiento es exactamente cero. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 4 — Pero hay una trampa Esto NO significa que: ❌ “Las computadoras siempre cuentan desde cero.” El indexado desde cero es una convención muy común. La encontramos en lenguajes como: C C++ Java JavaScript Python Pero no es una ley universal. Existen lenguajes, estructuras y APIs que utilizan otras bases de indexación. 👉 Lo importante es entender qué representa el índice en el contexto que estás usando. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 5 — Y de aquí nace un clásico 😅 Tienes: 5 elementos Pero los índices son: 0 1 2 3 4 Por eso solemos recorrerlos con una condición como: i < longitud y no: i <= longitud Porque con longitud = 5 : array[4] ✅ array[5] ❌ Equivocarte por una posición puede producir el famoso: ⚠️ off-by-one error Un pequeño +1 o -1 ... puede ser suficiente para romper tu lógica. Sígueme para más contenido de Backend 🚀 💬 Comenta tu experiencia o dudas. @hermanprimodev Slide 6 — array[0] ya no parece tan raro Cuando veas: array[0] no pienses: 🤨 “Dame el elemento número cero.” Piensa: 📍 “Dame el elemento que está a CERO posiciones del inicio.” [A] [B] [C] [D] [E] 0 1 2 3 4 ↑ 0 posiciones Ese pequeño cambio de perspectiva explica por qué el indexado desde cero encaja tan bien con desplazamientos. 🧠 El índice no tiene que representar cuántos elementos llevas contando. También puede representar qué tan lejos estás del comienzo. 🔥 El backend no se ve, pero sin él, nada funciona. Sígueme para más contenido de Backend 🚀 💬 ¿Recuerdas la primera vez que array[0] te hizo pensar “¿por qué esto empieza en cero?” 😅 @hermanprimodev Elegí construir el carrusel alrededor de un cambio de perspectiva: de “número de elemento” a “distancia desde el inicio” . Primero hacemos que el 0 parezca extraño y después usamos exactamente ese mismo 0 para que la explicación encaje visualmente. Dejé fuera más detalles históricos y de implementación porque el ejemplo de direcciones de memoria ya comunica la intuición principal. La precisión importante es que empezar desde cero no es una obligación del hardware ni de todos los lenguajes; es una convención muy extendida y especialmente natural cuando un índice se interpreta como desplazamiento.

14 sep 2026
Leer publicación
¿Por qué borrar un archivo no significa que desaparezca inmediatamente del disco?
Curiosidad tecnológica

¿Por qué borrar un archivo no significa que desaparezca inmediatamente del disco?

🗑️ Borras una fotografía de tu computadora… pero eso no siempre significa que sus datos desaparezcan inmediatamente Tienes una fotografía llamada: vacaciones.jpg La eliminas. Después vacías la papelera. La buscas nuevamente. Ya no aparece. Desde la perspectiva del usuario parece bastante claro: “El archivo ya no existe.” Pero internamente la historia puede ser diferente. Dependiendo del sistema de archivos, del dispositivo de almacenamiento y de cómo se haya realizado la eliminación, los datos que formaban ese archivo podrían permanecer físicamente almacenados durante cierto tiempo. Entonces aparece una pregunta interesante: 👉 ¿Qué significa realmente “borrar” un archivo? 🧠 Primero: guardar un archivo implica mucho más que almacenar sus bytes Cuando guardas algo como: vacaciones.jpg el sistema no simplemente coloca la fotografía en algún lugar del disco y espera que luego puedas encontrarla. Necesita mantener información que describa ese archivo. Conceptualmente podríamos imaginar algo así: vacaciones.jpg ↓ metadatos ↓ ubicación de los datos ↓ almacenamiento El sistema de archivos puede mantener información como: Nombre del archivo. Tamaño. Fechas. Permisos. Ubicación o referencias hacia sus datos. Estado del espacio utilizado. La implementación exacta depende del sistema de archivos. NTFS, ext4, APFS y otros utilizan estructuras diferentes. Pero la idea general permanece: 👉 El sistema necesita una manera de saber qué datos pertenecen a cada archivo. 📚 El sistema de archivos funciona un poco como un índice Imagina tener un disco lleno de información sin ningún mecanismo para organizarla. Tendrías millones de bloques de datos, pero no sabrías: ¿Cuáles pertenecen a foto.jpg? ¿Cuáles pertenecen a documento.pdf? ¿Cuáles están libres? ¿Cuáles forman parte del sistema? El sistema de archivos proporciona esa organización. Simplificando muchísimo: Archivo ↓ Sistema de archivos ↓ Referencia a bloques ↓ Datos físicos Cuando abres una fotografía, el sistema utiliza esa información para localizar las partes correspondientes y presentarlas como un único archivo. Por eso eliminar un archivo no necesariamente necesita comenzar destruyendo cada uno de sus bits. Puede empezar simplemente modificando esas referencias. ⚙️ ¿Qué ocurre cuando presionas “Eliminar”? Muchas veces existe primero una etapa intermedia. En sistemas de escritorio, cuando envías un archivo a la papelera, normalmente el archivo todavía sigue existiendo. Simplemente fue movido o marcado de manera que aparezca dentro de la papelera. Por eso puedes restaurarlo fácilmente. El flujo podría verse así: vacaciones.jpg ↓ 🗑️ Papelera ↓ Restaurar En este punto todavía no hablamos necesariamente de una eliminación definitiva. Cuando vacías la papelera o utilizas una eliminación que la omite, entonces el sistema puede marcar el archivo como eliminado. Pero incluso ahí no significa necesariamente: sobrescribir todos los datos con ceros inmediatamente 🧩 Muchas veces borrar significa liberar espacio lógico Supongamos que un archivo ocupa determinados bloques. Antes: Bloque 100 → vacaciones.jpg Bloque 101 → vacaciones.jpg Bloque 102 → vacaciones.jpg Después de eliminarlo, el sistema podría pasar a considerar: Bloque 100 → disponible Bloque 101 → disponible Bloque 102 → disponible La diferencia importante es esta: ANTES espacio perteneciente al archivo DESPUÉS espacio disponible para reutilización No necesariamente hubo que destruir en ese instante el contenido anterior. El sistema simplemente deja de considerar esos bloques como parte de un archivo válido. 🚀 Una analogía sencilla: una biblioteca Imagina una biblioteca enorme. En su catálogo aparece: Libro A → Estante 47 Cuando alguien solicita el libro, el bibliotecario consulta el catálogo y sabe exactamente dónde encontrarlo. Ahora imagina que decides eliminarlo del inventario. Podrías hacer: Eliminar: Libro A → Estante 47 y marcar: Estante 47 → disponible Desde el punto de vista administrativo: Libro A ya no existe. Pero físicamente el libro podría continuar ahí. Simplemente nadie debería buscarlo utilizando el catálogo. Algún tiempo después alguien coloca: Libro B en ese mismo estante. Ahora el contenido anterior sí fue reemplazado. Borrar archivos puede parecerse conceptualmente a esto en determinados sistemas. 🔎 ¿Entonces por qué existen programas para recuperar archivos borrados? Precisamente porque a veces existe una diferencia entre: El archivo dejó de estar registrado. y: Los datos fueron físicamente reemplazados. Si todavía existe suficiente información en el almacenamiento, una herramienta de recuperación puede intentar encontrarla. Hay varias posibilidades. Podría recuperar información sobre el archivo que todavía permanezca en estructuras del sistema de archivos. O podría buscar directamente patrones conocidos dentro de los datos. Por ejemplo, determinados formatos tienen estructuras identificables. Una herramienta puede buscar firmas que indiquen: Aquí probablemente comienza un JPEG. y tratar de reconstruir el contenido. 🧩 Recuperar el nombre y recuperar los datos no son exactamente lo mismo Imagina que eliminaste: vacaciones-cancun-2026.jpg Puede ocurrir que los datos de la imagen todavía estén presentes, pero la información original sobre: nombre ruta fecha ya no esté completamente disponible. Una herramienta podría recuperar entonces algo como: file000237.jpg El contenido sobrevivió. Pero parte de los metadatos utilizados para identificarlo se perdió. Por eso una recuperación puede producir archivos aparentemente completos con nombres completamente diferentes. 🧱 ¿Qué pasa si el archivo estaba fragmentado? Otro problema aparece cuando un archivo no estaba almacenado de manera completamente contigua. Imagina: vacaciones.jpg parte 1 → bloque 100 parte 2 → bloque 500 parte 3 → bloque 900 El sistema de archivos sabe cómo relacionar esas partes. Pero después de una eliminación, algunas de esas referencias podrían perderse. Una herramienta de recuperación necesitaría reconstruir correctamente: parte 1 + parte 2 + parte 3 Si una de ellas ya fue sobrescrita: parte 1 ✅ parte 2 ❌ parte 3 ✅ el archivo podría recuperarse parcialmente o quedar corrupto. Por eso recuperar un archivo eliminado no siempre significa obtenerlo exactamente como estaba. ⚠️ Por eso debes dejar de utilizar la unidad si borraste algo importante Supongamos que eliminaste accidentalmente un documento. El sistema ahora considera sus bloques: disponibles Después instalas un programa. Descargas videos. Copias fotografías. Actualizas el sistema. Cada una de esas acciones puede generar escrituras. El almacenamiento podría reutilizar justamente esos bloques. Conceptualmente: Documento eliminado ↓ espacio disponible ↓ descargas archivo nuevo ↓ parte del espacio se reutiliza ↓ datos antiguos sobrescritos Cuanto más escribes, mayores pueden ser las posibilidades de reemplazar información que todavía era recuperable. Por eso una recomendación habitual ante una eliminación accidental importante es: 👉 minimizar nuevas escrituras sobre esa unidad. 💿 En un disco duro tradicional, este modelo es relativamente intuitivo En un HDD, la información se almacena magnéticamente sobre platos. Simplificando mucho, el sistema puede marcar determinados sectores como disponibles sin necesidad de sobrescribirlos inmediatamente. Mientras esos sectores no sean reutilizados, parte de la información anterior puede permanecer. Por eso históricamente ha sido posible recuperar archivos eliminados de discos duros incluso después de vaciar la papelera. Pero esto no significa que la recuperación esté garantizada. Depende de: Cuánto tiempo pasó. Cuánto se utilizó el disco. Qué partes fueron sobrescritas. Qué información del sistema de archivos sobrevivió. El estado físico del dispositivo. ⚡ En un SSD la historia cambia bastante Los SSD no funcionan como discos duros. No tienen platos magnéticos ni cabezales. Utilizan memoria flash y un controlador que administra internamente dónde colocar la información. Y ahí aparece un mecanismo importante: TRIM . 🧠 ¿Qué es TRIM? Cuando eliminas un archivo, el sistema operativo sabe que determinados bloques lógicos ya no contienen información que necesites conservar. Pero el SSD también necesita saberlo. TRIM permite comunicarle algo conceptualmente parecido a: Estos bloques ya no contienen información válida que necesite conservarse. El SSD puede utilizar esa información para preparar espacio para futuras escrituras. Este comportamiento puede mejorar el rendimiento y la gestión interna del dispositivo. Pero tiene una consecuencia importante: 👉 La recuperación de archivos eliminados puede resultar mucho más difícil. 🔄 Un SSD no necesariamente escribe donde tú imaginas Supongamos que desde el sistema operativo ves: Bloque lógico 100 No significa necesariamente que exista una relación simple y permanente con una única posición física de memoria. El controlador del SSD utiliza mecanismos internos para administrar las celdas. Puede hacer cosas como: Wear leveling. Garbage collection. Remapeo de bloques. Gestión de páginas. Movimiento interno de datos. Todo esto ocurre para utilizar mejor la memoria flash y prolongar su vida útil. Por eso pensar: “Voy a sobrescribir exactamente el mismo sector varias veces” no funciona en SSD de la misma manera intuitiva que podría imaginarse con un HDD. 🧹 ¿Qué es Garbage Collection en un SSD? La memoria flash tiene restricciones importantes. Generalmente no puede simplemente sobrescribir cualquier pequeño fragmento directamente de la misma manera que otros tipos de almacenamiento. Las operaciones de borrado se realizan en unidades mayores. Por eso el controlador necesita reorganizar internamente información válida y liberar bloques. Conceptualmente: Bloque ├── Datos válidos ├── Datos eliminados ├── Datos eliminados └── Datos válidos El SSD puede copiar los datos todavía válidos a otro lugar y liberar el bloque completo. Este proceso se relaciona con el garbage collection . Si los datos eliminados son tratados como innecesarios, eventualmente pueden dejar de existir físicamente sin que tú escribas directamente sobre ellos. ⚠️ Esto explica por qué recuperar desde SSD puede ser mucho más complicado En un HDD tradicional podrías tener: archivo eliminado ↓ sectores no sobrescritos ↓ posible recuperación En un SSD con TRIM activo podría ocurrir: archivo eliminado ↓ TRIM ↓ SSD sabe que esos datos ya no son necesarios ↓ garbage collection ↓ datos eliminados internamente El tiempo exacto y el comportamiento dependen del dispositivo, sistema operativo y configuración. Por eso no deberías asumir: “Como acabo de eliminarlo, todavía estará físicamente ahí.” Con almacenamiento moderno, eso puede dejar de ser cierto mucho antes de lo esperado. 🔐 El cifrado cambia todavía más el problema Imagina un dispositivo cuyo almacenamiento está completamente cifrado. Los datos físicos podrían existir como bloques cifrados. Para interpretarlos necesitas una clave. Eso permite otra estrategia interesante de eliminación segura: 👉 destruir las claves necesarias para descifrar los datos. Esto suele conocerse como crypto erase o borrado criptográfico. Conceptualmente: Datos cifrados + Clave = Información legible Si destruyes correctamente la clave: Datos cifrados + ❌ clave = información prácticamente inutilizable Esto puede ser mucho más rápido que intentar sobrescribir físicamente enormes cantidades de almacenamiento. 🛡️ Por eso el cifrado completo del disco es importante Imagina que alguien roba una computadora. Si el disco no está cifrado, dependiendo del sistema y de cómo se hayan eliminado archivos, podría existir información recuperable. Con cifrado completo correctamente configurado, el atacante primero necesita superar la protección criptográfica. Herramientas como: BitLocker FileVault LUKS son ejemplos de tecnologías utilizadas para cifrar almacenamiento en diferentes sistemas. El cifrado no sustituye buenas políticas de seguridad, pero cambia radicalmente el riesgo asociado a datos residuales. 🗑️ “Eliminar” y “eliminar de forma segura” son operaciones diferentes Cuando utilizas el botón normal: Eliminar el objetivo principal suele ser: dejar de mostrar el archivo + recuperar su espacio Pero cuando necesitas eliminar información sensible, el objetivo cambia: hacer que recuperar los datos sea prácticamente inviable Son problemas diferentes. Por eso sistemas y dispositivos pueden ofrecer funciones especiales de borrado seguro. 🧹 ¿Sobrescribir muchas veces un archivo garantiza destruirlo? Existe una idea bastante popular: “Para eliminar algo de forma segura debes sobrescribirlo siete, diez o treinta veces.” En discos modernos esto suele ser una simplificación innecesaria. En discos magnéticos actuales, una sola sobrescritura adecuada suele ser suficiente para escenarios normales. Y en SSD existe un problema adicional: 👉 El controlador puede redirigir escrituras hacia celdas diferentes. Por ejemplo, tú podrías intentar: sobrescribir bloque lógico 100 pero internamente el SSD podría colocar esos nuevos datos en una ubicación física diferente por razones de wear leveling. Eso significa que una herramienta que intenta sobrescribir un archivo individual no necesariamente controla qué ocurre físicamente con cada copia anterior de esos datos. 🧰 ¿Qué usar entonces para borrar un SSD de forma segura? Depende del dispositivo, sistema operativo y nivel de seguridad requerido. Para eliminar completamente una unidad pueden existir opciones como: Secure Erase proporcionado por el dispositivo. Sanitize commands. Funciones del fabricante. Borrado criptográfico. Restablecimientos seguros. Herramientas específicas del sistema. No existe un comando universal que sea correcto para todos los SSD. En entornos donde la información es especialmente sensible, también pueden existir procedimientos de destrucción física. La estrategia debe corresponder al riesgo. ☁️ ¿Y qué ocurre con archivos almacenados en la nube? La situación se vuelve todavía más interesante. Cuando borras algo de un servicio en la nube, puede existir: Archivo visible ↓ Papelera ↓ Eliminación lógica ↓ Sistemas internos ↓ Replicación ↓ Backups No significa necesariamente que todos los bytes desaparezcan instantáneamente de cada dispositivo físico del proveedor. Los servicios suelen tener políticas de retención, replicación, respaldos y eliminación. Por eso debes distinguir entre: Ya no accesible para el usuario y: eliminado definitivamente de toda infraestructura El segundo proceso puede depender de políticas internas y periodos de retención. 📱 También ocurre en teléfonos Cuando borras una fotografía en un smartphone muchas veces sucede primero: Foto ↓ Eliminados recientemente ↓ 30 días ↓ eliminación Mientras se encuentra en esa sección, recuperarla puede ser trivial. Después de eliminarla definitivamente, la posibilidad de recuperación dependerá del almacenamiento, cifrado, TRIM y comportamiento interno del dispositivo. Los smartphones modernos utilizan almacenamiento flash, por lo que las ideas de un viejo disco duro no siempre se aplican directamente. 🧩 El sistema de archivos también puede conservar información adicional Algunos sistemas utilizan mecanismos como: Journaling. Snapshots. Versiones. Copias de seguridad. Esto significa que eliminar un archivo de su ubicación principal no siempre implica que ninguna otra copia exista. Por ejemplo, podrías borrar: /documentos/reporte.pdf pero conservar una versión dentro de: snapshot de ayer Esto es muy útil para recuperación. Pero también importa cuando tu objetivo es destruir información sensible. Eliminar la copia principal no necesariamente elimina todas las versiones históricas. 📸 ¿Por qué a veces una recuperación encuentra miniaturas aunque no encuentre la fotografía? Muchos sistemas y aplicaciones generan archivos derivados. Por ejemplo: foto-original.jpg ↓ miniatura ↓ preview ↓ cache Puedes eliminar el archivo original pero continuar teniendo una miniatura guardada en otra ubicación. Eso significa que una herramienta forense podría encontrar: preview pequeño ✅ archivo original ❌ La información puede haber dejado rastros en diferentes lugares. Por eso la eliminación segura de información sensible puede ser bastante más compleja que borrar un único archivo. 🔍 Recuperación normal vs análisis forense También conviene separar dos conceptos. Una herramienta doméstica de recuperación puede buscar archivos borrados relativamente recientes. Un análisis forense profesional puede intentar reconstruir mucha más información utilizando: Estructuras del sistema de archivos. Fragmentos. Metadatos. Cachés. Snapshots. Logs. Copias temporales. Eso no significa que todo archivo eliminado sea recuperable. Pero sí demuestra que: 👉 “No aparece en el explorador” no equivale automáticamente a “no queda ningún rastro”. ⚠️ Error común: instalar el programa de recuperación en el mismo disco Imagina que eliminaste accidentalmente: tesis.docx y enseguida descargas un programa de recuperación de 500 MB al mismo disco. Acabas de generar nuevas escrituras. Incluso la instalación puede crear: archivos cachés logs temporales y alguno de ellos puede reutilizar espacio donde estaba la tesis. Cuando el archivo es realmente importante, lo ideal suele ser reducir al mínimo cualquier escritura sobre la unidad afectada y trabajar desde otra unidad o entorno cuando sea posible. ⚠️ Tampoco conviene ejecutar reparaciones sin entender qué hacen Si una unidad parece dañada y contiene información importante, ejecutar inmediatamente herramientas que modifiquen el sistema de archivos puede cambiar estructuras necesarias para recuperación. En casos críticos: datos importantes + fallo físico o corrupción puede ser preferible trabajar primero sobre una copia o imagen del dispositivo. Especialmente si el disco hace ruidos, desaparece o presenta fallos físicos, seguir utilizándolo puede empeorar la situación. 🛠️ Buenas prácticas si borraste accidentalmente algo importante De forma general: ✔️ Deja de utilizar la unidad tanto como sea posible. ✔️ Evita copiar archivos nuevos allí. ✔️ No instales herramientas de recuperación en esa misma unidad. ✔️ Comprueba primero papelera, backups, snapshots y servicios de nube. ✔️ Si el dispositivo está fallando físicamente, evita pruebas destructivas. ✔️ Si los datos son realmente críticos, considera recuperación profesional. Y, sobre todo: 👉 No dependas de la recuperación como estrategia de respaldo. 💾 La mejor recuperación sigue siendo tener un backup Los programas de recuperación existen porque las cosas salen mal. Pero confiar en ellos es apostar a que los datos no hayan sido sobrescritos o eliminados definitivamente. Un backup evita esa incertidumbre. Conceptualmente: Archivo original ↓ eliminación accidental ❌ Backup ↓ restaurar ✅ Para archivos importantes, es mucho más seguro mantener copias independientes que esperar que una herramienta pueda reconstruir información eliminada. 🔐 Y si vas a vender o regalar un dispositivo, borrar tus carpetas no es suficiente Imagina que vas a vender una laptop. Haces: Seleccionar todo ↓ Eliminar ↓ Vaciar papelera Eso no debería considerarse automáticamente una sanitización segura del dispositivo. Una estrategia adecuada depende del almacenamiento y del sistema. En equipos modernos suele ser recomendable utilizar las funciones oficiales de restablecimiento seguro, borrado del dispositivo o herramientas proporcionadas para ese hardware, especialmente si la unidad estaba cifrada. El objetivo debería ser asegurarte de que el siguiente propietario no pueda acceder razonablemente a tu información anterior. 🧠 Hay tres niveles que conviene distinguir Podemos resumir el concepto con tres estados diferentes. 1. Archivo visible vacaciones.jpg ✅ El sistema sabe dónde está y puedes abrirlo. 2. Archivo eliminado vacaciones.jpg ❌ espacio reutilizable ✅ Ya no aparece normalmente, pero parte de sus datos podría continuar presente. 3. Datos destruidos o inaccesibles datos sobrescritos o bloques eliminados o clave criptográfica destruida En este punto recuperar la información puede ser inviable. La transición entre el segundo y el tercer estado depende mucho del tipo de almacenamiento. 🧩 La realidad Cuando presionas: Eliminar no necesariamente estás ordenando: “Encuentra cada bit de este archivo y destrúyelo físicamente ahora mismo.” Muchas veces estás diciendo algo mucho más parecido a: Este archivo ya no debe formar parte del sistema de archivos. El espacio que ocupaba puede reutilizarse. Después, lo que ocurra físicamente depende de múltiples capas: Sistema operativo ↓ Sistema de archivos ↓ Controlador ↓ Tipo de almacenamiento ↓ HDD / SSD / flash En un HDD, los datos pueden permanecer hasta que otros los sobrescriban. En un SSD, mecanismos como TRIM y garbage collection pueden hacer que desaparezcan de otra manera. Con cifrado, incluso puede ser suficiente destruir una clave para volver los datos inutilizables. Y si existen snapshots, backups o cachés, podrían sobrevivir copias adicionales. Por eso “borrar” no es una única operación universal. 💬 En almacenamiento, “ya no puedo verlo” y “los datos dejaron de existir” son dos afirmaciones completamente diferentes. 👉 ¿Alguna vez eliminaste accidentalmente una fotografía o documento y lograste recuperarlo después de vaciar la papelera? 🔥 El backend no se ve, pero sin él, nada funciona.

13 sep 2026
Leer publicación
Git merge vs rebase: hacen algo parecido, pero pueden cambiar por completo el historial de tu proyecto
Herramientas Developer

Git merge vs rebase: hacen algo parecido, pero pueden cambiar por completo el historial de tu proyecto

🌿 Tienes una rama con varios días de trabajo y necesitas integrarla al proyecto Tu compañero te dice: Haz merge. Otro desarrollador responde: Mejor haz rebase. Los dos caminos pueden terminar llevando tus cambios hacia la rama principal. Pero no hacen exactamente lo mismo. Y, sobre todo, no dejan la misma historia. Entonces aparece una pregunta bastante común cuando empiezas a trabajar seriamente con Git: 👉 ¿Qué diferencia realmente a git merge de git rebase ? 🧠 Partamos de dos ramas Imagina este historial: A — B — C ← main \ D — E ← feature La rama feature nació desde B . Mientras trabajabas en ella, main recibió el commit C . Ahora quieres integrar ambos trabajos. Tienes al menos dos estrategias muy conocidas: git merge o git rebase El resultado final puede contener cambios equivalentes, pero la forma en que Git registra la historia será distinta. 🔀 Opción 1: git merge Con merge , Git conserva ambas líneas de desarrollo y crea un punto donde se unen. Podrías hacer: git switch main git merge feature Y terminar con algo parecido a: A — B — C —— M \ / D — E M es un merge commit. Ese commit tiene dos padres: C E y representa explícitamente el momento en que ambas ramas se combinaron. Git no necesita fingir que la rama feature nunca se separó. Al contrario. La historia muestra claramente: main avanzó por un lado feature avanzó por otro y después se integraron 📖 Merge conserva la historia tal como ocurrió Esta es una de las mayores características de merge . Los commits: D E siguen siendo exactamente los mismos commits. No cambian sus identificadores. La historia conserva la bifurcación original. Conceptualmente: B ├── C └── D — E ↓ merge Esto puede ser útil cuando quieres entender: Cuándo se creó una rama. Qué commits pertenecían a una funcionalidad. En qué momento se integró. Qué líneas de desarrollo existieron en paralelo. El historial puede ser más ramificado, pero también más fiel a lo ocurrido. ⚡ ¿Siempre se crea un merge commit? No. Git también puede hacer un fast-forward merge . Imagina este caso: A — B ← main \ C — D ← feature Si main no avanzó desde que creaste feature , no hay dos historias que realmente necesiten fusionarse. Git puede simplemente mover el puntero de main : A — B — C — D ↑ main Eso se conoce como fast-forward . Puedes incluso forzar la creación de un merge commit usando opciones como: git merge --no-ff feature si el equipo quiere conservar explícitamente el hecho de que existió una rama. 🧹 Opción 2: git rebase rebase funciona de otra manera. En lugar de unir dos historias conservando la bifurcación, toma los commits de tu rama y los vuelve a aplicar sobre otra base. Partimos de: A — B — C ← main \ D — E ← feature Desde feature podrías ejecutar: git rebase main Git conceptualmente hace algo parecido a: 1. Identifica D y E 2. Los separa temporalmente 3. Mueve feature encima de C 4. Reaplica los cambios de D 5. Reaplica los cambios de E El resultado sería: A — B — C — D' — E' Ahora parece que el trabajo de feature comenzó después de C . El historial queda lineal. ⚠️ D' y E' no son los mismos commits Este punto es fundamental. Aunque: D' pueda contener prácticamente los mismos cambios que: D Git los considera commits diferentes. ¿Por qué? Porque el identificador de un commit depende de información como: Su contenido. Su padre. Sus metadatos. El árbol asociado. Antes: D → padre B Después del rebase: D' → padre C Cambió el padre. Por lo tanto cambia también el hash. Lo mismo ocurre con los commits siguientes. Por eso decimos que rebase : 👉 reescribe la historia . 🧬 ¿Por qué el hash cambia? En Git un commit no es simplemente: "estos archivos cambiaron" También forma parte de una cadena histórica. Podemos imaginarlo así: Commit E ↓ apunta a D ↓ apunta a B Si cambias la base: E' ↓ apunta a D' ↓ apunta a C ya no estás hablando de la misma cadena de commits. Aunque el código final sea equivalente, la historia es diferente. 🔀 Merge une historias; Rebase mueve historia Una forma sencilla de recordarlo es: MERGE Conserva: A — B — C \ D — E y las une. Mientras que: REBASE Toma: D — E y los vuelve a colocar después de: C Por eso ambos sirven para integrar trabajo, pero lo hacen con filosofías distintas. ⚔️ Entonces… ¿cuál es mejor? No existe una respuesta universal. Depende del flujo de trabajo del equipo. merge Tiene ventajas como: ✔️ Conserva commits existentes. ✔️ No reescribe historia. ✔️ Es más seguro con ramas compartidas. ✔️ Muestra claramente cuándo se integraron líneas de desarrollo. Pero también puede producir un historial más complejo: * Merge feature-x |\ | * commit 3 | * commit 2 | * commit 1 * | |/ * otro commit Si existen muchas ramas y muchos merges, leer git log puede ser más difícil. 🧹 Rebase puede hacer la historia más sencilla de leer Después de rebase puedes obtener: A — B — C — D' — E' En lugar de: A — B — C —— M \ / D — E Esto puede facilitar cosas como: git log --oneline porque los commits aparecen en una secuencia más lineal. También puede hacer más sencillo revisar cómo evolucionó una funcionalidad. Pero ese beneficio tiene un precio: 👉 estás creando nuevos commits. ⚠️ La regla más importante de Rebase Hay una regla práctica muy conocida: Evita hacer rebase de commits públicos que otras personas ya están utilizando. ¿Por qué? Supongamos que publicas: D — E Tu compañero descarga esos commits. Ahora tú haces: git rebase main y terminas con: D' — E' Tu historial ahora dice: A — B — C — D' — E' Pero tu compañero todavía tiene: A — B \ D — E Desde la perspectiva de Git: D != D' E != E' Aunque representen trabajo parecido. Ahora existen dos versiones distintas de la misma historia. 💥 ¿Qué ocurre cuando intentas hacer push? Después de un rebase, es común que un push normal sea rechazado. Por ejemplo: git push puede fallar porque el historial remoto contiene: D — E mientras tú ahora tienes: D' — E' Git no puede simplemente avanzar la rama. Para reemplazar el historial remoto tendrías que hacer un push forzado. Muchas veces se recomienda: git push --force-with-lease en lugar de: git push --force porque --force-with-lease intenta protegerte de sobrescribir cambios remotos que no esperabas. Aun así, sigue siendo una operación que debe utilizarse con cuidado. 🚨 ¿Por qué git push --force puede ser peligroso? Imagina que tu rama remota tiene: D — E — F Tu compañero agregó F . Pero tú solamente conoces: D — E Después haces rebase: D' — E' y ejecutas: git push --force Podrías terminar reemplazando: D — E — F por: D' — E' El commit F desaparece de la referencia remota. Por eso --force-with-lease suele ser una opción más prudente. Antes de sobrescribir, comprueba que el remoto siga en el estado que esperabas. 🏡 Rebase suele ser mucho más cómodo en ramas privadas Supongamos que creaste: feature/login y solamente tú estás trabajando allí. Puedes tener commits como: WIP fix otro fix cambio rápido Mientras tanto main avanza. Antes de abrir tu Pull Request puedes hacer: git fetch origin git rebase origin/main Ahora tu trabajo queda encima de los cambios más recientes. Si nadie más depende de esos commits, reescribirlos es mucho menos problemático. Por eso una regla práctica común es: Historia local o privada → rebase puede ser cómodo. Historia pública y compartida → mucho más cuidado. No es una ley absoluta, pero ayuda bastante. 🔄 ¿Qué sucede cuando hay conflictos? Tanto merge como rebase pueden producir conflictos. Supongamos que main cambió: const timeout = 5000; Mientras tu rama cambió la misma línea: const timeout = 10000; Git no puede decidir automáticamente qué versión debería sobrevivir. Con merge , podría detenerse y pedirte resolver el conflicto antes de crear el merge commit. Con rebase , Git puede detenerse mientras reaplica uno de tus commits. El flujo típico sería: git rebase main Git detecta conflicto. Lo resuelves. Después: git add archivo.js git rebase --continue Si quieres cancelar todo: git rebase --abort y vuelves al estado anterior al rebase. 🧩 Una diferencia importante al resolver conflictos Con un merge normalmente resuelves el conflicto en el punto de integración. Conceptualmente: C \ M ← resolver diferencias / E Con rebase, Git reaplica tus commits uno por uno. Si tienes: D E F G podrías encontrar conflictos durante varios de esos commits. Eso puede ser útil porque Git te obliga a resolver la historia paso a paso. Pero también puede resultar más laborioso en ramas grandes. 🧠 Rebase también puede ser interactivo Existe una herramienta muy poderosa: git rebase -i o: git rebase --interactive Por ejemplo: git rebase -i HEAD~4 Git puede mostrar: pick a1b2c3 Crear componente pick d4e5f6 Corregir estilo pick g7h8i9 Fix pick j1k2l3 Otro fix Y puedes decidir: pick reword edit squash fixup drop Esto permite reorganizar commits antes de compartirlos. 🧹 ¿Para qué sirve squash ? Supongamos que tienes: Agregar login Corregir login Fix login Ahora sí funciona login Antes de integrar podrías convertir todo en: Agregar autenticación de usuarios Con rebase interactivo: pick A Agregar login squash B Corregir login squash C Fix login squash D Ahora sí funciona login Git combina esos commits. Esto puede producir una historia más fácil de entender. Pero nuevamente: 👉 estás reescribiendo commits. Por eso suele hacerse antes de que otras personas comiencen a depender de ellos. 🧾 ¿Qué es un historial “limpio”? Decir: “Rebase deja un historial limpio” puede ser demasiado subjetivo. Para algunos equipos, esto: A — B — C — D — E — F es limpio porque es lineal. Para otros equipos: A — B —— M \ / C-D es más informativo porque muestra cómo se desarrollaron las funcionalidades. La limpieza depende de qué información quieres conservar. Un historial extremadamente lineal puede ser fácil de leer. Un historial con merges puede explicar mejor la estructura real del trabajo. 🧭 ¿Qué estrategia utilizan los equipos? Hay muchísimas. Un equipo podría trabajar así: feature branch ↓ commits locales ↓ rebase sobre main ↓ Pull Request ↓ merge Otro: feature branch ↓ Pull Request ↓ merge commit Otro puede usar: squash merge y convertir todo el Pull Request en un único commit. Otro puede utilizar: rebase and merge desde plataformas como GitHub. No existe un único flujo correcto. Lo importante es comprender qué historia produce cada opción. 🔀 ¿Qué es Squash Merge? Muchas plataformas permiten integrar una rama tomando todos sus cambios y convirtiéndolos en un único commit. Supongamos: feature: D — E — F Al hacer squash merge podrías terminar en main con: A — B — C — S donde S contiene los cambios combinados de: D + E + F Esto produce un historial principal muy compacto. Puede ser útil cuando los commits internos de una Pull Request son solamente pasos temporales de desarrollo. 🧠 Merge commit vs Squash Merge vs Rebase Merge Podemos imaginar tres resultados distintos. Merge commit A — B — C —— M \ / D — E Conserva la estructura de la rama. Squash merge A — B — C — S Combina los cambios de la rama en un único commit. Rebase and merge A — B — C — D' — E' Reaplica cada commit linealmente sobre main . Los tres pueden producir un árbol de archivos final parecido. Pero la historia queda diferente. 🛠️ Una estrategia práctica para una rama personal Imagina que estás trabajando solo en: feature/productos Mientras desarrollas haces: Ajustar modelo Agregar endpoint Fix endpoint Agregar validación Fix validación Mientras tanto main recibe nuevos cambios. Podrías hacer: git fetch origin git rebase origin/main Resolver cualquier conflicto. Luego revisar: git log --oneline Y, si quieres, utilizar: git rebase -i para mejorar tus commits antes de abrir el Pull Request. Este uso de rebase suele ser bastante cómodo porque todavía controlas completamente esa historia. ⚠️ Una estrategia menos recomendable Imagina una rama: develop utilizada activamente por 15 desarrolladores. Todos hacen: git pull y crean trabajo desde ella. Hacer rebase de toda esa rama y forzar el historial remoto puede provocar bastante caos. Cada desarrollador tendrá referencias hacia los commits antiguos. Después tendrán que reconciliar sus ramas con los nuevos commits. No significa que sea imposible. Significa que el costo y el riesgo son mucho mayores. 🧠 ¿Y qué hace git pull ? Este punto también suele causar confusión. Cuando ejecutas: git pull Git normalmente combina dos operaciones: git fetch + integración Según tu configuración, esa integración puede realizarse mediante merge o rebase. Por ejemplo: git pull --rebase obtiene los cambios remotos y vuelve a aplicar tus commits locales encima. Supongamos: Remoto: A — B — C Local: A — B — D Con pull --rebase podrías terminar: A — B — C — D' en vez de crear un merge commit local. ⚙️ Puedes configurar el comportamiento de git pull Si un equipo prefiere rebase: git config pull.rebase true Si prefiere merge: git config pull.rebase false También existen otras configuraciones. Lo importante es saber qué está haciendo Git. Ejecutar comandos sin entender si están haciendo merge o rebase es una fuente común de historiales inesperados. 🧩 ¿Qué ocurre con git bisect y el debugging? El historial no sirve solamente para verse bonito. También es una herramienta de diagnóstico. Comandos como: git bisect permiten buscar qué commit introdujo un problema. Un historial con commits pequeños y coherentes puede facilitar mucho esa tarea. Por ejemplo: A ✅ B ✅ C ✅ D ❌ E ❌ Git puede ayudarte a identificar: D como el posible commit problemático. Por eso, ya sea que uses merge o rebase, vale la pena pensar en la calidad de los commits. Un historial lineal lleno de: fix fix2 ahora sí final final-final no necesariamente es mejor que un historial con ramas bien organizadas. 🔍 ¿Qué es más fácil de revisar? Depende. Un historial lineal permite leer: Commit 1 ↓ Commit 2 ↓ Commit 3 ↓ Commit 4 sin saltos. Pero los merge commits pueden aportar contexto: Aquí se integró feature/pagos Aquí se integró feature/reportes Aquí se integró hotfix/login Algunos equipos prefieren usar merges para representar unidades de trabajo. Otros prefieren que main sea completamente lineal. Ambas decisiones pueden ser razonables. ⚠️ Error común: usar rebase solamente porque “se ve más profesional” Un historial bonito no compensa un equipo confundido. Antes de usar rebase deberías entender: ¿Qué commits voy a reescribir? ¿Ya están publicados? ¿Alguien más depende de ellos? ¿Tendré que hacer force push? ¿Estoy preparado para resolver conflictos? Si no conoces las respuestas, quizá merge sea una opción más segura. El objetivo no es utilizar el comando más sofisticado. Es conservar una historia comprensible sin poner en riesgo el trabajo compartido. ⚠️ Error común: pensar que Merge nunca genera problemas Merge tampoco es perfecto. Si creas constantemente commits como: Merge branch 'main' into feature Merge main Merge main otra vez Merge main nuevamente puedes terminar con un historial extremadamente ruidoso. Por eso algunos equipos prefieren actualizar ramas con rebase antes de abrir un Pull Request. Otros aceptan esos merges porque priorizan no reescribir historia. La estrategia depende de las convenciones. 🛠️ Una guía sencilla Si estás trabajando en una rama solamente tuya y quieres actualizarla con los últimos cambios: git fetch origin git rebase origin/main puede ser una opción cómoda. Si estás integrando una rama compartida y quieres preservar su historia: git merge puede ser más apropiado. Si necesitas convertir una Pull Request con muchos commits temporales en una sola unidad lógica: Squash merge puede resultar útil. Pero siempre revisa primero las reglas del proyecto. 🧪 Un ejemplo completo Partimos de: A — B — C ← main \ D — E ← feature Con merge Ejecutas: git switch main git merge feature Resultado: A — B — C —— M \ / D — E Los commits originales sobreviven. Con rebase Desde feature : git rebase main Resultado: A — B — C — D' — E' Después puedes integrar mediante fast-forward: git switch main git merge feature Resultado final: A — B — C — D' — E' El código puede terminar prácticamente igual. La historia no. 🧩 La realidad git merge y git rebase no son simplemente dos comandos diferentes para hacer “lo mismo”. Ambos pueden integrar líneas de desarrollo, pero responden a preguntas distintas. merge dice: Estas ramas existieron por separado y aquí fue donde se unieron. rebase dice: Voy a tomar estos commits y volverlos a aplicar sobre una base más reciente. Uno conserva la topología original. El otro reorganiza parte de la historia. Y por eso elegir entre ambos no consiste únicamente en conseguir que el código compile. También estás decidiendo: 👉 ¿Cómo quieres que Git recuerde la evolución de tu proyecto? Un historial lineal puede ser muy cómodo. Un historial con merges puede conservar información valiosa. Lo importante es que el equipo entienda la estrategia y la aplique de forma consistente. 💬 Un historial limpio puede ayudar muchísimo, pero entender qué commits estás modificando es mucho más importante que hacer que el gráfico se vea bonito. 👉 En tus proyectos, ¿prefieres conservar explícitamente las ramas con merge o mantener main más lineal utilizando rebase ? 🔥 El backend no se ve, pero sin él, nada funciona.

12 sep 2026
Leer publicación
¿Qué pasa si dos servidores intentan modificar el mismo dato al mismo tiempo?
Arquitectura de Software

¿Qué pasa si dos servidores intentan modificar el mismo dato al mismo tiempo?

🚨 Dos servidores leen exactamente el mismo dato… y uno de los cambios desaparece Imagina una aplicación con varias instancias del backend atendiendo peticiones al mismo tiempo. Dos usuarios realizan una operación casi simultáneamente. El estado actual es: stock = 10 Servidor A lee: stock = 10 Servidor B también lee: stock = 10 Ambos necesitan descontar una unidad. Servidor A calcula: 10 - 1 = 9 Servidor B calcula exactamente lo mismo: 10 - 1 = 9 Después ambos guardan: stock = 9 Pero ocurrieron dos ventas. El resultado correcto debería ser: stock = 8 👉 ¿Dónde desapareció una de las modificaciones? Bienvenido a uno de los problemas clásicos de concurrencia: la actualización perdida o lost update . 🧠 El problema no es tener varios servidores Tener múltiples instancias del backend es completamente normal. De hecho, es una estrategia muy común para distribuir tráfico y escalar una aplicación. Podríamos tener algo así: ┌── Backend A Usuario ──────┼── Backend B └── Backend C ↓ Base de datos El problema aparece cuando dos o más procesos intentan modificar el mismo estado compartido sin suficiente coordinación. Cada servidor puede estar funcionando perfectamente. Cada petición puede tener una lógica aparentemente correcta. Pero cuando las operaciones ocurren al mismo tiempo, empiezan los problemas. ⚙️ ¿Cómo ocurre una actualización perdida? El error suele aparecer cuando hacemos una secuencia como esta: 📖 Leer valor ↓ 🧮 Calcular nuevo valor ↓ 💾 Guardar resultado Con una sola petición no hay problema. Por ejemplo: stock = 10 ↓ 10 - 1 ↓ stock = 9 Pero con dos peticiones concurrentes puede ocurrir esto: Tiempo Servidor A Servidor B T1 lee stock = 10 T2 lee stock = 10 T3 calcula 9 T4 calcula 9 T5 guarda 9 T6 guarda 9 Ambas operaciones parten del mismo estado. La segunda escritura no sabe que la primera ya modificó el registro. El resultado final termina siendo: stock = 9 cuando debería ser: stock = 8 Una de las modificaciones se perdió. 🧮 El problema está en el famoso “leer → modificar → escribir” Este patrón aparece muchísimo en aplicaciones. Por ejemplo: producto = obtener_producto(id) producto.stock = producto.stock - 1 guardar(producto) A primera vista parece completamente correcto. Pero existen tres pasos distintos: 1. Leer 2. Calcular 3. Guardar Entre el paso 1 y el paso 3 otra petición puede modificar exactamente el mismo registro. Y ahí aparece la condición de carrera. 🏁 ¿Qué es una condición de carrera? Una race condition ocurre cuando el resultado depende del orden o del momento exacto en que varias operaciones concurrentes se ejecutan. Por ejemplo: Petición A ────────┐ ├── mismo dato Petición B ────────┘ Si A termina antes que B, obtienes un resultado. Si B termina antes que A, quizá obtengas otro. Y si ambas leen el mismo estado antes de escribir, puedes obtener un resultado incorrecto. Lo peligroso es que muchas veces el error no aparece durante desarrollo. Con una sola persona probando manualmente: click espera click espera todo funciona. En producción: Usuario 1 ─┐ Usuario 2 ─┤ Usuario 3 ─┤→ mismo recurso Usuario 4 ─┘ la historia cambia por completo. 🚀 No ocurre solamente con inventarios El stock es un ejemplo fácil de visualizar, pero el problema puede aparecer en muchos lugares. 💰 Saldos Imagina: saldo = 1000 Dos operaciones retiran 100 al mismo tiempo. Ambas leen: 1000 Ambas calculan: 900 Y ambas guardan: 900 Pero deberían quedar: 800 En un sistema financiero esto sería un error gravísimo. 🎟️ Disponibilidad Supongamos que queda: 1 asiento Dos usuarios intentan comprarlo casi al mismo tiempo. Si ambos leen: disponible = true antes de que uno de ellos termine la compra, podrías terminar vendiendo el mismo asiento dos veces. 📅 Reservas Un calendario muestra un horario libre: 10:00 - 11:00 Dos usuarios realizan la reserva simultáneamente. Si solamente haces: consultar disponibilidad ↓ crear reserva sin proteger la operación completa, ambos podrían superar la primera validación. 📊 Contadores Algo aparentemente inocente como: visitas = visitas + 1 también puede sufrir el mismo problema si varias peticiones realizan el cálculo fuera de una operación segura. 📦 Estados de pedidos Dos procesos podrían modificar un pedido simultáneamente. Por ejemplo: Proceso A: Pendiente → Pagado Proceso B: Pendiente → Cancelado Si ambos parten del mismo estado anterior, necesitas definir qué transición tiene prioridad y cuáles son válidas. La concurrencia no solamente puede perder números. También puede producir estados de negocio contradictorios. ✏️ También puede ocurrir cuando dos personas editan el mismo registro Imagina un sistema administrativo. Dos empleados abren al mismo cliente. La información inicial es: nombre: Ana estado: Pendiente telefono: 5551234 Empleado A cambia: estado: Aprobado Mientras tanto, empleado B modifica: telefono: 5559999 pero su formulario todavía contiene: estado: Pendiente Si B envía todo el registro: { "nombre": "Ana", "estado": "Pendiente", "telefono": "5559999" } puede sobrescribir accidentalmente el cambio de A. Resultado: telefono actualizado ✅ estado volvió a Pendiente ❌ Este tipo de conflicto también es una forma muy común de actualización perdida. 🛠️ ¿Cómo evitamos una actualización perdida? No existe una única estrategia correcta. Depende de cosas como: Qué tan crítica es la información. Cuánta concurrencia existe. Cuánto tiempo dura la operación. Si los conflictos son frecuentes. Qué experiencia quieres para el usuario. Pero hay varias herramientas importantes. ⚡ Opción 1: operaciones atómicas Volvamos al inventario. En lugar de hacer: leer stock ↓ stock - 1 ↓ guardar podemos pedirle directamente a la base de datos: UPDATE producto SET stock = stock - 1 WHERE id = 123; Aquí la operación se expresa directamente sobre el valor existente en la base de datos. No estamos haciendo: backend lee 10 backend calcula 9 backend envía 9 Estamos diciendo: Base de datos: "Resta 1 al valor actual." Eso cambia muchísimo el problema. La base de datos puede ejecutar esa modificación de forma atómica. 🧱 ¿Qué significa “atómica”? Una operación atómica se comporta conceptualmente como una unidad indivisible. Desde el punto de vista de otras operaciones, no existe un estado intermedio parcialmente aplicado. Por ejemplo: UPDATE contador SET valor = valor + 1 WHERE id = 1; Si varias peticiones ejecutan esta sentencia, la base de datos puede coordinar correctamente las modificaciones al mismo registro. Esto suele ser mucho más seguro que: valor = obtener_contador() valor += 1 guardar(valor) cuando existe concurrencia. ✅ Incluso puedes hacer la validación dentro de la misma operación Supongamos que no quieres permitir stock negativo. En lugar de: 1. consultar stock 2. comprobar stock > 0 3. descontar podrías usar una operación condicional: UPDATE producto SET stock = stock - 1 WHERE id = 123 AND stock > 0; Después puedes comprobar cuántas filas fueron modificadas. Si fue: 1 fila la reserva de stock funcionó. Si fue: 0 filas quizá ya no había disponibilidad. Esto reduce la ventana donde otra operación puede interponerse entre la comprobación y la modificación. 🔒 Opción 2: bloqueos Otra estrategia consiste en bloquear temporalmente el recurso mientras una operación crítica está trabajando con él. Por ejemplo: Transacción A ↓ Bloquea producto 123 ↓ Lee stock ↓ Modifica stock ↓ Confirma transacción ↓ Libera bloqueo Mientras tanto: Transacción B ↓ Intenta acceder al mismo registro ↓ ⏳ espera Dependiendo de la base de datos y del tipo de bloqueo utilizado, puedes impedir que operaciones conflictivas modifiquen simultáneamente el mismo dato. 🧠 Pessimistic Locking Este enfoque suele llamarse control de concurrencia pesimista . La idea es: “Existe una posibilidad suficiente de conflicto, así que voy a bloquear el recurso antes de modificarlo.” Conceptualmente: 🔒 bloquear ↓ 📖 leer ↓ 🧮 calcular ↓ 💾 modificar ↓ ✅ confirmar ↓ 🔓 liberar En SQL, dependiendo del motor, puede aparecer algo como: SELECT stock FROM producto WHERE id = 123 FOR UPDATE; normalmente dentro de una transacción. La sintaxis y el comportamiento exacto dependen del sistema de base de datos. ⚠️ Los bloqueos también tienen un costo Bloquear todo indiscriminadamente tampoco es una buena estrategia. Si mantienes bloqueos durante demasiado tiempo: Petición A 🔒 bloquea registro ↓ hace trabajo lento... ↓ llama API externa... ↓ procesa archivo... ↓ 10 segundos después 🔓 libera otras operaciones pueden quedarse esperando innecesariamente. Eso puede aumentar: Latencia. Contención. Uso de conexiones. Riesgo de deadlocks . Dificultad para escalar. Por eso las secciones críticas deberían ser lo más pequeñas posible. 🔄 Opción 3: control de concurrencia optimista Existe otra estrategia muy útil cuando los conflictos no ocurren constantemente. En lugar de bloquear el registro desde el principio, permitimos que varios usuarios lo lean. Pero antes de guardar comprobamos que nadie haya modificado el dato mientras trabajábamos. Por ejemplo: producto stock = 10 version = 7 El usuario carga esa versión. Después intenta actualizar diciendo: "Modifica este registro, pero solamente si sigue en version = 7." La consulta podría ser conceptualmente: UPDATE producto SET stock = 9, version = 8 WHERE id = 123 AND version = 7; Si alguien más ya actualizó el registro: version = 8 entonces la condición: version = 7 ya no coincide. Resultado: 0 registros actualizados Y acabas de detectar un conflicto. 🚦 ¿Qué hacer cuando detectas el conflicto? Detectarlo es solamente la mitad del problema. Después debes decidir qué hacer. Dependiendo de la operación puedes: ✔️ Reintentar automáticamente. ✔️ Volver a leer el estado actual. ✔️ Mostrar un mensaje al usuario. ✔️ Recalcular la operación. ✔️ Rechazar el cambio. ✔️ Pedir al usuario que revise las modificaciones. Por ejemplo, en un editor administrativo podrías mostrar: ⚠️ Este registro fue modificado por otro usuario. Tu versión: Pendiente Versión actual: Aprobado y permitir decidir cómo resolverlo. 🧠 Optimista vs pesimista Podemos resumirlos así: PESIMISTA "Creo que puede haber conflicto." ↓ Bloqueo antes de modificar OPTIMISTA "Probablemente no habrá conflicto, pero comprobaré antes de guardar." ↓ Detectar conflicto al actualizar Ninguno es universalmente mejor. Por ejemplo: Un asiento de avión muy demandado puede necesitar una estrategia diferente a la edición ocasional del nombre de un cliente. La elección depende del tipo de recurso y de las reglas del negocio. 🧾 También puedes utilizar timestamps o ETags La versión numérica: version = 7 es fácil de entender. Pero no es la única opción. En algunos sistemas puedes utilizar: updated_at o mecanismos como ETag . Por ejemplo, una API puede entregar: ETag: "registro-v7" Y el cliente intenta actualizar enviando: If-Match: "registro-v7" Si la versión actual ya cambió, el servidor puede rechazar la modificación. Esto permite aplicar control de concurrencia también a nivel HTTP. ⚠️ Error común: “Estoy usando transacciones, entonces ya estoy protegido” No necesariamente. Una transacción es una herramienta fundamental, pero el resultado depende de qué operaciones incluye y del nivel de aislamiento. Por ejemplo, podrías tener: Transacción A 📖 lee stock = 10 Transacción B 📖 lee stock = 10 Dependiendo del motor, del nivel de aislamiento y de cómo realizas las actualizaciones, ambos procesos todavía podrían trabajar inicialmente con información que entra en conflicto. Las transacciones permiten agrupar operaciones y proporcionar garantías importantes. Pero no convierten automáticamente cualquier lógica en segura frente a toda condición de carrera. 🧩 El nivel de aislamiento también importa Las bases de datos relacionales suelen ofrecer diferentes niveles de aislamiento. Conceptualmente, cuanto mayor es el aislamiento, más intenta comportarse el sistema como si las transacciones no estuvieran interfiriendo entre sí. Pero mayores garantías también pueden implicar más coordinación. Por eso existen diferentes niveles, como: Read Committed Repeatable Read Serializable Los nombres exactos son conocidos en SQL, aunque el comportamiento concreto puede variar entre motores. Serializable , por ejemplo, intenta ofrecer garantías muy fuertes, pero no significa que debas activarlo para absolutamente todo sin analizar costos y necesidades. 🔥 Serializable no elimina la necesidad de diseñar el comportamiento Incluso con garantías fuertes, una aplicación debe estar preparada para situaciones como: Transacción no puede completarse ↓ conflicto detectado ↓ rollback ↓ reintentar La concurrencia no siempre se resuelve consiguiendo que todas las operaciones “pasen”. A veces la solución correcta consiste precisamente en detectar que dos operaciones son incompatibles y hacer que una de ellas falle de forma controlada. 🛒 Ejemplo práctico: comprar la última unidad Supongamos: stock = 1 Dos usuarios presionan “Comprar”. Podemos usar una operación condicional: UPDATE producto SET stock = stock - 1 WHERE id = 123 AND stock > 0; La primera petición podría actualizar: 1 fila y dejar: stock = 0 La segunda intenta exactamente lo mismo. Pero ahora: stock > 0 ya no se cumple. Entonces actualiza: 0 filas El backend puede responder: 409 Conflict o una respuesta apropiada según el diseño de la API: { "error": "Producto sin disponibilidad" } No evitamos que las dos personas intentaran comprar. Coordinamos correctamente qué ocurre cuando compiten por el mismo recurso. 💳 Pero cuidado con operaciones de negocio más complejas Ahora supongamos que comprar implica: 1. Reservar stock 2. Crear pedido 3. Procesar pago 4. Confirmar compra Ya no estamos modificando solamente una columna. Si bloqueas stock durante toda una llamada a un proveedor de pagos que tarda varios segundos, quizá estés introduciendo otros problemas. Muchas aplicaciones separan conceptos como: stock disponible stock reservado stock vendido y utilizan expiraciones o procesos de compensación. Por ejemplo: Usuario inicia compra ↓ Reserva unidad durante 10 minutos ↓ Pago exitoso ↓ Reserva → venta Si el pago falla o expira: Reserva ↓ se libera ↓ stock vuelve a estar disponible La solución de concurrencia debe seguir las reglas reales del negocio. 📅 Otro ejemplo: reservar una cita Imagina una tabla: citas doctor_id fecha hora Podrías validar desde el backend: ¿Existe cita a las 10:00? ↓ No ↓ INSERT Pero dos peticiones podrían ejecutar la comprobación simultáneamente. Una protección adicional muy poderosa puede ser una restricción en la propia base de datos. Por ejemplo, conceptualmente: UNIQUE (doctor_id, fecha, hora) Ahora, aunque ambas peticiones intenten crear la misma reserva, la base de datos impide que existan dos registros incompatibles. Esto muestra otra idea importante: 👉 No toda protección de concurrencia tiene que implementarse únicamente con código del backend. 🧱 Las restricciones de base de datos también son parte de la defensa Las reglas críticas deberían protegerse lo más cerca posible del dato cuando sea razonable. Por ejemplo: UNIQUE CHECK FOREIGN KEY NOT NULL pueden impedir determinados estados inválidos. Supongamos que la regla es: “Un asiento solamente puede pertenecer a una reserva activa.” Si esa regla puede expresarse mediante restricciones o un diseño de datos adecuado, tienes una capa adicional de protección. No sustituye toda la lógica del negocio. Pero ayuda a evitar que un bug de aplicación termine dejando datos imposibles. 🌐 ¿Y qué cambia cuando tengo varios servidores? Curiosamente, muchas de estas técnicas funcionan precisamente porque la coordinación ocurre en un lugar compartido. Por ejemplo: Backend A ─┐ ├──→ Base de datos Backend B ─┘ Si ambos utilizan correctamente: Operaciones atómicas. Transacciones. Bloqueos. Versiones. Restricciones. no importa tanto cuál instancia recibió la petición. La base de datos funciona como punto de coordinación para ese estado. Eso es mucho más seguro que intentar mantener una variable local como: lock = True dentro de un único proceso. ⚠️ Un lock en memoria puede dejar de funcionar cuando escalas Supongamos que haces: Backend A 🔒 lock local Eso puede impedir que dos threads dentro de Backend A modifiquen simultáneamente algo. Pero ahora tienes: Backend A 🔒 lock A Backend B 🔒 lock B Cada servidor tiene su propio lock. Backend B no sabe que Backend A bloqueó algo. Entonces un mecanismo que funcionaba con una sola instancia deja de resolver el problema cuando escalas horizontalmente. La coordinación debe ocurrir en un sistema compartido si quieres proteger operaciones entre procesos diferentes. 🧠 No todos los datos necesitan la misma consistencia Aquí aparece una distinción importante. Perder una actualización en: contador aproximado de vistas quizá no tenga el mismo impacto que perder una actualización en: saldo bancario o: último asiento disponible Por eso no tiene sentido aplicar siempre el mecanismo más pesado posible a cada dato. Debes preguntarte: ¿Qué pasa si dos operaciones coinciden? ¿Puedo reintentar? ¿Puedo tolerar pequeñas diferencias? ¿Debo impedir absolutamente el conflicto? ¿Existe una única respuesta válida? La respuesta determina cuánto control necesitas. 🕒 Otro problema importante: operaciones que tardan demasiado Cuanto mayor sea el tiempo entre: leer y: escribir más grande es la ventana en la que puede aparecer un conflicto. Por ejemplo: Usuario abre formulario ↓ lee version = 7 ↓ lo deja abierto 25 minutos ↓ otra persona modifica el registro ↓ primer usuario finalmente guarda Aquí no estamos hablando de milisegundos. El mismo problema puede aparecer después de minutos. Por eso el control optimista resulta especialmente útil en interfaces donde las personas editan datos durante bastante tiempo. 🔁 ¿Debemos reintentar siempre? No. Reintentar puede ser útil para operaciones técnicas donde volver a ejecutar es seguro. Pero en algunos casos un reintento automático puede cambiar el significado del negocio. Supongamos que: stock = 1 Dos personas compran. Una gana. La otra recibe conflicto. Reintentar no puede fabricar una segunda unidad. En cambio, una operación como incrementar un contador puede ser más adecuada para un reintento automático. El sistema necesita distinguir entre: Conflicto temporal solucionable y: Regla de negocio que ya no puede cumplirse 🧩 ¿Qué pasa con sistemas todavía más distribuidos? Hasta ahora hablamos principalmente de varios backends compartiendo una base de datos. Pero los sistemas pueden ser mucho más complejos: Servicio A ↓ Servicio B ↓ Servicio C ↓ varias bases de datos En ese escenario coordinar cambios simultáneos puede ser todavía más difícil. No siempre existe una única transacción que abarque todo. Pueden aparecer estrategias como: Reservas. Compensaciones. Procesamiento asíncrono. Máquinas de estados. Detección de versiones. Operaciones diseñadas para poder repetirse de forma segura. Pero eso ya depende enormemente de la arquitectura. La idea fundamental sigue siendo la misma: 👉 Cuando varios actores modifican estado compartido, necesitas definir explícitamente cómo se resuelven los conflictos. ⚠️ Errores comunes al diseñar concurrencia Uno muy frecuente es asumir: “La posibilidad de que dos usuarios hagan clic exactamente al mismo tiempo es muy pequeña.” No necesitan hacerlo en el mismo microsegundo. Solamente necesitan coincidir dentro de la ventana entre leer y escribir. Otro error es pensar: “Como tengo una base de datos seria, ella resolverá cualquier problema automáticamente.” La base de datos proporciona herramientas. La aplicación debe utilizarlas correctamente. También es común: Mantener bloqueos demasiado tiempo. No comprobar cuántas filas modificó un UPDATE . Sobrescribir registros completos innecesariamente. No definir restricciones críticas en la base de datos. Reintentar operaciones que no deberían repetirse. Probar únicamente con una petición a la vez. 🧪 ¿Cómo pruebas este tipo de errores? La concurrencia necesita pruebas diferentes a las habituales. No basta con: hacer petición ↓ comprobar resultado Puedes ejecutar muchas operaciones simultáneas. Por ejemplo: stock inicial = 100 Lanzas: 100 peticiones concurrentes cada una descontando una unidad. Esperas: stock final = 0 Si terminas con: stock = 37 tienes un problema bastante evidente. Este tipo de pruebas puede revelar bugs que nunca aparecen realizando peticiones manuales una detrás de otra. 🛠️ ¿Qué estrategia debería utilizar? Una guía conceptual podría ser: ¿La modificación puede expresarse como una operación atómica? ↓ Sí ↓ Hazlo directamente en la base de datos. Si necesitas leer información y tomar varias decisiones: ¿Los conflictos son frecuentes y necesito exclusividad? ↓ Control pesimista / bloqueo Si los conflictos son poco frecuentes: Permite trabajo concurrente ↓ Comprueba versión al guardar ↓ Control optimista Y para reglas críticas: Añade también restricciones en la base de datos cuando sea posible. No es una receta universal, pero ayuda a pensar correctamente sobre el problema. 🧩 La realidad Cuando una aplicación crece, ya no puedes asumir que una sola petición estará trabajando con un dato a la vez. Puedes tener: 🖥️ Múltiples servidores ⚙️ Múltiples procesos 👥 Miles de usuarios 🔄 Operaciones simultáneas Y cada proceso puede ejecutar código perfectamente válido desde su propia perspectiva. El problema aparece cuando todos comparten el mismo estado. Ese es uno de los grandes desafíos de la concurrencia: Código correcto de forma aislada + ejecución simultánea = resultado potencialmente incorrecto Por eso diseñar un backend no consiste solamente en preguntarse: “¿Esta operación funciona?” También debes preguntarte: 👉 ¿Sigue funcionando si otra operación intenta modificar exactamente lo mismo al mismo tiempo? En inventarios, reservas, saldos, contadores y estados importantes, esa pregunta puede ser la diferencia entre un sistema que parece funcionar y uno que realmente mantiene sus datos correctos bajo carga. 💬 En sistemas concurrentes, el problema no siempre es que una petición falle. A veces ambas funcionan aparentemente bien y justamente por eso el dato termina mal. 👉 Si dos usuarios modificaran ahora mismo el mismo registro de tu aplicación, ¿tu sistema detectaría el conflicto o simplemente ganaría la última escritura? 🔥 El backend no se ve, pero sin él, nada funciona.

11 sep 2026
Leer publicación
¿Por qué una página puede verse rápido aunque todavía no esté lista para usarse?
Frontend

¿Por qué una página puede verse rápido aunque todavía no esté lista para usarse?

⚡ Entras a una página y parece cargar casi instantáneamente Ya puedes ver: 🖼️ Imágenes. 📝 Textos. 🔘 Botones. 📋 Formularios. Todo parece estar listo. Intentas presionar un botón y... Nada. Un segundo después vuelves a intentarlo. Ahora sí funciona. Esa sensación puede parecer extraña porque, desde tu perspectiva, la página ya estaba ahí. Entonces surge una pregunta importante: 👉 ¿Cómo puede una página estar visible pero todavía no estar completamente lista para usarse? Una posible explicación está en un proceso muy común en aplicaciones web modernas: Hydration . 🧠 ¿Qué es Hydration? En algunas arquitecturas web, el servidor puede enviar al navegador HTML que ya contiene parte de la interfaz renderizada. Eso significa que el navegador no siempre necesita esperar a que JavaScript construya absolutamente todo desde cero para comenzar a mostrar contenido. Puede recibir algo parecido a esto: <h1>Producto</h1> <p>$499</p> <button>Agregar al carrito</button> El navegador sabe cómo dibujar ese HTML inmediatamente. Por eso el usuario puede empezar a ver la página relativamente pronto. Pero existe un detalle: Ese HTML puede representar solamente la parte visual inicial. La lógica interactiva todavía puede necesitar JavaScript. Ahí entra la hidratación. De forma simplificada: Servidor genera HTML ↓ Navegador recibe el documento ↓ 👀 El usuario ve contenido ↓ JavaScript se descarga ↓ JavaScript se interpreta y ejecuta ↓ Framework conecta la lógica ↓ ✅ La interfaz queda interactiva La página puede verse antes de estar completamente preparada para responder a todas las acciones del usuario. 💧 ¿Por qué se llama “Hydration”? El nombre puede sonar un poco extraño. La idea es imaginar que el servidor envía una estructura que ya existe visualmente, pero que todavía necesita recuperar la lógica y el estado necesarios para funcionar como una aplicación interactiva. Por ejemplo, el servidor podría enviar: <button>Comprar</button> El navegador puede mostrar ese botón perfectamente. Pero el HTML por sí solo no sabe necesariamente qué debe ocurrir cuando haces clic. La lógica podría existir en JavaScript: function comprar() { agregarAlCarrito(); } Durante la hidratación, el framework relaciona la interfaz que ya existe en el documento con la aplicación que se está ejecutando en el navegador. Conceptualmente: HTML visible + Código JavaScript + Estado de la aplicación ↓ Interfaz interactiva Es como si la estructura ya estuviera construida y después se conectaran los mecanismos que permiten utilizarla. ⚙️ ¿Qué ocurre realmente en el navegador? Imagina una aplicación creada con un framework que soporta renderizado desde el servidor. Cuando visitas una página pueden ocurrir varias cosas. Primero, el navegador solicita el documento: GET /productos/123 El servidor responde con HTML. Ese HTML ya puede incluir: Nombre del producto Precio Imagen Descripción Botón de compra El navegador comienza a procesarlo y mostrarlo. Hasta aquí, el usuario ya puede pensar: “La página terminó de cargar.” Pero paralelamente todavía puede faltar descargar archivos JavaScript: /app.js /product-page.js /cart.js Después el navegador necesita: 📥 Descargar esos archivos. 🧩 Analizarlos. ⚙️ Ejecutarlos. 🧠 Reconstruir la información que necesita el framework. 🔗 Relacionar componentes con el HTML ya existente. Y solamente entonces determinadas interacciones quedan completamente disponibles. 🚀 Un ejemplo práctico: una tienda online Imagina que visitas una página de producto. El servidor genera inicialmente: 📸 Imagen del producto 📦 Nombre 💰 Precio ⭐ Calificación 🔘 Botón "Agregar al carrito" El navegador recibe ese HTML y puede mostrarlo casi inmediatamente. La interfaz parece lista. Pero el botón depende de código JavaScript que debe ejecutar algo parecido a: async function agregarAlCarrito(productoId) { await fetch("/api/carrito", { method: "POST", body: JSON.stringify({ producto_id: productoId }) }); } Mientras ese código todavía no está preparado, el botón puede existir visualmente sin tener todavía toda su funcionalidad conectada. Entonces existen dos momentos distintos: Momento 1 👀 Puedo ver la página. Momento 2 🖱️ Puedo interactuar correctamente con ella. Y esos momentos no necesariamente ocurren al mismo tiempo. 🔘 HTML visible no significa JavaScript listo Este es uno de los puntos más importantes para entender Hydration. El navegador sabe interpretar HTML sin necesidad de frameworks como React. Si recibe: <button>Guardar</button> puede mostrar un botón. Pero mostrarlo no significa que exista automáticamente una función asociada a él. Algo similar ocurre con: 📋 Formularios complejos. 🧭 Menús desplegables. ❤️ Botones de favoritos. 🛒 Carritos de compra. 🔍 Buscadores con autocompletado. 🎛️ Filtros dinámicos. El navegador puede mostrar la estructura, pero la aplicación todavía puede necesitar ejecutar JavaScript para activar el comportamiento esperado. ⚛️ ¿Cómo encaja React en todo esto? En una aplicación React que utiliza renderizado desde el servidor, el servidor puede generar HTML a partir de los componentes. Simplificando mucho, podríamos tener un componente como: function Comprar() { return ( <button onClick={() => console.log("Comprado")}> Comprar </button> ); } El servidor puede producir inicialmente algo parecido a: <button>Comprar</button> Ese HTML llega al navegador. Pero el navegador no recibe el onClick directamente como una función ejecutable dentro de ese HTML. React necesita ejecutarse en el cliente y relacionar ese nodo existente con el componente correspondiente. Después de la hidratación, React ya puede manejar la interacción. La idea importante es esta: 👉 React intenta reutilizar el HTML que ya fue renderizado en lugar de volver a construir toda la interfaz desde cero. 🔄 Hydration no es lo mismo que renderizar nuevamente toda la página Si el servidor ya produjo: <h1>Mi producto</h1> <button>Comprar</button> sería desperdiciar trabajo eliminar todo eso y volver a crearlo completamente desde JavaScript. La hidratación intenta aprovechar la estructura existente. De forma conceptual: HTML del servidor ↓ React encuentra estructura equivalente ↓ Conecta componentes y lógica ↓ Continúa trabajando desde ahí Por eso servidor y cliente necesitan estar suficientemente sincronizados respecto a lo que esperan renderizar. ⚠️ ¿Qué es un Hydration Mismatch? Aquí aparece un problema bastante conocido. Supongamos que el servidor genera: <p>Buenos días</p> Pero cuando React comienza a ejecutarse en el navegador, espera: <p>Buenas noches</p> Ahora existe una diferencia entre lo que produjo el servidor y lo que espera el cliente. Eso puede provocar un hydration mismatch . Otro ejemplo común sería generar contenido utilizando directamente valores que pueden cambiar entre servidor y navegador: const ahora = new Date(); El servidor podría renderizar: 10:59:59 y unos milisegundos después el navegador producir: 11:00:00 Dependiendo del framework y la situación, estas diferencias pueden producir advertencias, correcciones adicionales o incluso obligar a reconstruir partes de la interfaz. Por eso la hidratación también exige cierta consistencia. 🧠 El estado también debe tener sentido en ambos lados Imagina una aplicación que renderiza desde el servidor: Carrito: 3 productos Cuando JavaScript comienza a ejecutarse, el cliente necesita arrancar desde un estado compatible con lo que el usuario ya está viendo. Si el cliente cree inicialmente: Carrito: 0 productos pueden aparecer cambios visuales inesperados. Por eso algunas arquitecturas transmiten al navegador información necesaria para reconstruir el estado inicial. Conceptualmente: Servidor ↓ HTML + Datos iniciales ↓ Navegador ↓ Framework ↓ Hydration No se trata solamente de conectar botones. También puede involucrar reconstruir correctamente la aplicación alrededor del contenido ya renderizado. ⏱️ Ver rápido y responder rápido son métricas diferentes Tradicionalmente era común pensar: “La página cargó cuando apareció el contenido.” Pero la experiencia real es más compleja. Puedes tener una página que muestre algo muy rápido: 0.8 segundos → contenido visible pero que no responda correctamente hasta: 2.5 segundos → interacción disponible Desde una perspectiva técnica, la página puede parecer rápida. Desde la perspectiva del usuario: “Presioné el botón y no funcionó.” Y esa percepción importa muchísimo. Por eso el rendimiento frontend no solamente depende de cuánto tarda en aparecer contenido. También importa cuánto tarda la página en estar preparada para responder. 📦 El problema de enviar demasiado JavaScript Uno de los principales factores que puede retrasar la interactividad es la cantidad de JavaScript enviada al navegador. Imagina una página que descarga: 50 KB de JavaScript y otra que descarga: 2 MB de JavaScript No solamente cambia el tiempo de descarga. Ese código también necesita ser: Descargado ↓ Descomprimido ↓ Analizado ↓ Compilado ↓ Ejecutado ↓ Utilizado para hidratar componentes Por eso reducir JavaScript puede tener beneficios incluso en conexiones rápidas. La red es solamente una parte del costo. 📱 En dispositivos lentos el problema se vuelve más evidente Supongamos que pruebas una aplicación en una laptop moderna. Procesador rápido. Buena conexión. Memoria suficiente. La hidratación tarda tan poco que apenas la notas. Pero después alguien abre la misma página desde: 📱 Un teléfono económico. 📶 Una conexión móvil inestable. 🔋 Un dispositivo trabajando en modo de ahorro de energía. Ahora interpretar y ejecutar JavaScript puede tardar significativamente más. Esto explica por qué evaluar únicamente desde nuestra computadora de desarrollo puede producir una impresión engañosa. Una aplicación debería medirse también bajo condiciones más cercanas a las de usuarios reales. 🚧 El hilo principal del navegador también tiene trabajo JavaScript normalmente compite por recursos dentro del navegador con otras tareas. El navegador necesita: Interpretar HTML. Procesar CSS. Calcular estilos. Dibujar la interfaz. Ejecutar JavaScript. Responder a eventos. Actualizar elementos visuales. Si ejecutamos demasiado JavaScript durante el arranque, podemos bloquear temporalmente el hilo principal. Mientras está ocupado, el navegador puede tardar más en responder a interacciones. Conceptualmente: Usuario hace clic ↓ Evento esperando ↓ JavaScript pesado sigue ejecutándose ↓ Termina el trabajo ↓ Evento puede procesarse Entonces incluso una interfaz que técnicamente ya tiene lógica conectada puede sentirse lenta si el navegador continúa demasiado ocupado. 🧩 No toda la página necesita necesariamente Hydration Esta es una idea importante en arquitecturas modernas. Imagina un artículo como este. La mayoría de su contenido es: 📝 Texto. 🖼️ Imágenes. 📚 Encabezados. Eso no necesita necesariamente JavaScript para funcionar. Quizá solamente tengas: ❤️ Botón de favorito 💬 Comentarios 🔍 Buscador 🌙 Selector de tema Esos pequeños elementos sí necesitan interactividad. Entonces aparece una pregunta arquitectónica interesante: 👉 ¿Por qué enviar JavaScript para toda la página si solamente una pequeña parte realmente lo necesita? Algunos frameworks modernos permiten reducir la cantidad de componentes que necesitan ejecutarse en el cliente. 🏝️ El concepto de “islas de interactividad” Una estrategia relacionada consiste en mantener la mayor parte de una página como contenido estático y solamente activar pequeñas áreas interactivas. Conceptualmente: ┌──────────────────────────────┐ │ Contenido estático │ │ │ │ Texto │ │ Imagen │ │ Descripción │ │ │ │ ┌────────────────┐ │ │ │ 🛒 Carrito │ │ │ │ Interactivo │ │ │ └────────────────┘ │ │ │ └──────────────────────────────┘ En lugar de hidratar absolutamente todo, solamente determinadas “islas” necesitan JavaScript. No todos los frameworks implementan exactamente el mismo modelo, pero la idea general es útil: 👉 Enviar interactividad solamente donde realmente aporta valor. ⚙️ Server Components también cambian cuánto JavaScript necesita el cliente En ecosistemas modernos como React y Next.js han aparecido enfoques donde determinados componentes pueden ejecutarse únicamente en el servidor. Eso permite que partes de la interfaz no necesiten convertirse automáticamente en JavaScript ejecutándose en el navegador. Conceptualmente: Server Component ↓ Renderiza contenido ↓ No necesita interactividad cliente Client Component ↓ Necesita estado / eventos / APIs del navegador ↓ JavaScript en el cliente Por ejemplo, un componente que solamente muestra: function Titulo({ producto }) { return <h1>{producto.nombre}</h1>; } puede no necesitar lógica interactiva en el cliente. Mientras que un componente como: function Contador() { const [cantidad, setCantidad] = useState(0); return ( <button onClick={() => setCantidad(cantidad + 1)}> {cantidad} </button> ); } sí necesita ejecutarse allí. La idea es reducir el área interactiva cuando sea posible. 🔄 Existen diferentes estrategias de Hydration No todas las aplicaciones tienen que hidratar toda la interfaz inmediatamente. Dependiendo del framework y arquitectura pueden existir estrategias como: Hydration completa Gran parte de la aplicación se hidrata al cargar. Hydration selectiva Algunas partes pueden priorizarse antes que otras. Hydration progresiva La interactividad puede ir activándose gradualmente. Hydration bajo demanda Ciertos componentes pueden esperar hasta que realmente sean necesarios. Las implementaciones concretas varían bastante entre herramientas y versiones. Pero todas intentan resolver una idea parecida: 👉 No hacer todo el trabajo de JavaScript de golpe si no es necesario. 👀 ¿Qué debería priorizarse primero? Imagina una página larga. En la parte superior tienes: 🔍 Buscador 🛒 Carrito 🔐 Iniciar sesión Y mucho más abajo: ⭐ Reseñas 📊 Gráficas 💬 Comentarios No necesariamente todas esas funciones tienen la misma prioridad. Tiene sentido que el navegador dedique recursos primero a aquello con lo que probablemente vas a interactuar inmediatamente. El resto puede esperar dependiendo de la arquitectura. Esta forma de pensar ayuda a diseñar aplicaciones que no solamente cargan rápido, sino que utilizan mejor el trabajo disponible en el navegador. ⚠️ Error común: pensar que SSR resuelve automáticamente el rendimiento Puedes pensar: “Estoy renderizando desde el servidor, entonces mi aplicación ya es rápida.” No necesariamente. El servidor puede mejorar el tiempo en que aparece contenido, pero si después envías una enorme cantidad de JavaScript, todavía puedes tener problemas de interactividad. Podrías tener: HTML aparece rápido ✅ JavaScript enorme ❌ Hydration lenta ❌ Interacción tardía ❌ SSR y Hydration forman parte del panorama, pero no garantizan por sí solos una buena experiencia. 📊 ¿Cómo medir si realmente existe un problema? No conviene optimizar solamente por intuición. Puedes utilizar herramientas de rendimiento del navegador y métricas web para entender qué ocurre realmente. Una métrica especialmente relacionada con la capacidad de respuesta es Interaction to Next Paint (INP) . INP intenta reflejar qué tan rápido responde una página a las interacciones del usuario durante su visita. No mide exclusivamente Hydration, pero una cantidad excesiva de JavaScript y trabajo en el hilo principal puede afectar la capacidad de responder. También resulta útil analizar: Tamaño de bundles JavaScript. Long Tasks. Uso del hilo principal. Tiempo de ejecución de scripts. Rendimiento bajo CPU limitada. Rendimiento en redes más lentas. El objetivo no es simplemente conseguir una puntuación. Es identificar qué está retrasando la experiencia. 🛠️ ¿Cómo podemos reducir el costo de Hydration? No existe una solución única, pero hay varias prácticas que pueden ayudar dependiendo de la aplicación. 1. Enviar menos JavaScript Si una funcionalidad no necesita ejecutarse en el navegador, evita convertirla innecesariamente en código cliente. 2. Mantener pequeños los componentes interactivos En lugar de volver interactiva toda una sección enorme por un solo botón, puede ser conveniente aislar la lógica. Por ejemplo: Página completa ├── Texto estático ├── Imagen estática ├── Descripción estática └── Botón interactivo 3. Dividir código No toda funcionalidad necesita descargarse al inicio. Herramientas de code splitting permiten cargar determinadas partes solamente cuando son necesarias. 4. Evitar trabajo pesado al arrancar Si ejecutas cálculos costosos inmediatamente después de cargar la página, puedes competir directamente con el trabajo necesario para volverla interactiva. 5. Mantener consistencia entre servidor y cliente Evita producir HTML diferente en ambos lados cuando no sea necesario. Esto reduce problemas de Hydration Mismatch. 6. Probar en hardware menos potente Una página que funciona perfectamente en tu computadora puede comportarse de manera completamente diferente en dispositivos modestos. 🚀 Ejemplo de una arquitectura más eficiente Imagina una tienda online. Podrías dividir conceptualmente la página así: Página de producto │ ├── Server │ ├── Nombre │ ├── Precio │ ├── Descripción │ ├── Características │ └── Información SEO │ └── Cliente ├── Selector de cantidad ├── Botón agregar al carrito ├── Favoritos └── Galería interactiva En lugar de enviar toda la página como una gran aplicación JavaScript, puedes reservar el trabajo cliente para las partes que realmente necesitan interacción. Esto puede reducir: 📦 JavaScript enviado. ⚙️ Trabajo de ejecución. 💧 Trabajo de hidratación. ⏱️ Tiempo hasta responder. No siempre será la arquitectura correcta para todos los proyectos, pero muestra cómo pensar en el problema. 🧩 La realidad Cuando una página aparece en tu pantalla, eso no significa necesariamente que el navegador haya terminado todo el trabajo. Puede haber ocurrido esto: HTML llegó ↓ Contenido apareció ↓ 👀 "Parece cargada" ↓ JavaScript sigue trabajando ↓ Hydration ↓ Eventos conectados ↓ 🖱️ "Ahora sí puedo usarla" La diferencia puede durar unos pocos milisegundos y pasar completamente desapercibida. O puede durar suficiente tiempo como para que el usuario presione un botón y piense que la aplicación está rota. Por eso una interfaz rápida no debería medirse únicamente preguntando: “¿Cuándo apareció?” También deberíamos preguntar: 👉 ¿Cuándo pudo el usuario interactuar con ella sin fricción? Hydration es una de esas piezas de frontend que normalmente permanece invisible cuando todo funciona bien. Pero cuando hacemos demasiado trabajo en el navegador, esa pequeña diferencia entre ver y usar puede convertirse en uno de los problemas más perceptibles de toda la experiencia. 💬 Una buena experiencia web no solamente muestra contenido rápido. También responde rápido cuando el usuario intenta utilizarlo. 👉 ¿Alguna vez intentaste presionar un botón apenas apareció una página y tuviste que hacerlo nuevamente porque todavía no estaba lista? 🔥 El backend no se ve, pero sin él, nada funciona.

10 sep 2026
Leer publicación
¿Cómo puede una aplicación guardar millones de imágenes y archivos sin llenar sus servidores?
Arquitectura de Software

¿Cómo puede una aplicación guardar millones de imágenes y archivos sin llenar sus servidores?

📸 Subes una foto a una aplicación… pero ¿dónde termina realmente ese archivo? Cambias tu foto de perfil. Seleccionas una imagen desde tu teléfono. Presionas “Guardar”. Unos segundos después, la fotografía aparece en tu cuenta y puedes verla desde cualquier dispositivo. Parece algo sencillo. Pero ahora imagina una plataforma con millones de usuarios haciendo exactamente lo mismo. Todos los días pueden subir: 🖼️ Fotografías. 🎥 Videos. 📄 Documentos. 🎧 Audios. 📦 Copias de seguridad. 🗂️ Archivos generados por la propia aplicación. Cuando estás construyendo un proyecto pequeño, guardar esos archivos directamente en el disco del servidor puede parecer perfectamente razonable. Por ejemplo: /app/uploads/perfiles/foto.webp El problema aparece cuando la aplicación crece. Miles de archivos se convierten en millones. Un servidor se convierte en varios. Y ese sencillo directorio /uploads empieza a convertirse en una dependencia importante de toda la arquitectura. 👉 ¿Cómo almacenan entonces las aplicaciones grandes cantidades de archivos sin convertir sus servidores de backend en enormes discos duros? Una de las respuestas más comunes es el almacenamiento de objetos u Object Storage . 🧠 ¿Qué es el almacenamiento de objetos? Object Storage es un modelo diseñado para almacenar grandes cantidades de datos no estructurados como imágenes, videos, documentos, audios o respaldos. En lugar de depender del disco donde está ejecutándose tu aplicación, utilizas un sistema especializado para almacenar esos archivos. Servicios como Amazon S3, Google Cloud Storage, Azure Blob Storage o soluciones compatibles como MinIO utilizan este tipo de enfoque. La idea general es separar dos responsabilidades: Backend ↓ Lógica de la aplicación Object Storage ↓ Archivos Tu backend continúa decidiendo quién puede subir una imagen, qué formatos acepta, qué usuario es propietario del archivo o qué permisos existen. Pero ya no necesita almacenar físicamente todos esos datos en su propio disco. 📦 ¿Qué significa que un archivo sea un “objeto”? En un almacenamiento tradicional solemos pensar en: Carpeta └── Subcarpeta └── archivo.jpg En Object Storage, el modelo conceptual es diferente. Un objeto normalmente contiene o está asociado con elementos como: 📦 Datos El contenido del archivo 🏷️ Metadatos Información relacionada con el objeto 🔑 Clave Un identificador utilizado para localizarlo Por ejemplo, una imagen podría tener una clave como: usuarios/847/perfil.webp También puede existir información asociada como su tipo de contenido: Content-Type: image/webp Además, los objetos suelen almacenarse dentro de un contenedor lógico que, dependiendo del proveedor, puede recibir nombres como bucket o container . ⚙️ ¿Qué ocurre cuando subes una foto? Imagina una aplicación donde el usuario cambia su fotografía de perfil. Un flujo sencillo podría ser: 👤 Usuario ↓ 🌐 Frontend ↓ ⚙️ Backend ↓ ☁️ Object Storage ↓ 🔑 Clave del objeto ↓ 🗄️ Base de datos El usuario selecciona la imagen. El backend recibe la solicitud y puede validar aspectos como: Si el usuario está autenticado. Si tiene permiso para realizar la operación. Si el archivo supera el tamaño permitido. Si el formato está permitido. Qué nombre o clave utilizará. Después, el archivo puede almacenarse en Object Storage. El almacenamiento podría devolver o permitir construir una referencia como: usuarios/847/perfil.webp Y esa referencia puede relacionarse con el usuario dentro de la base de datos. 🗄️ La base de datos no necesita contener la imagen completa Este punto es importante. Supongamos que tenemos una tabla de usuarios. Podríamos guardar algo conceptualmente parecido a: id: 847 nombre: "Carlos" foto_perfil: "usuarios/847/perfil.webp" La base de datos almacena información estructurada sobre el usuario y una referencia al archivo. Mientras tanto: Base de datos ↓ "usuarios/847/perfil.webp" Object Storage ↓ 📸 Datos reales de la fotografía Eso no significa que sea técnicamente imposible guardar archivos binarios dentro de una base de datos relacional. Existen tipos de datos que lo permiten y escenarios donde puede tener sentido. Pero para grandes cantidades de imágenes, videos y archivos similares, separar los archivos hacia almacenamiento especializado suele simplificar la escalabilidad y operación del sistema. 💾 ¿Por qué no guardar simplemente todo en el servidor? Al principio puede funcionar perfectamente. Tienes una aplicación pequeña: Servidor ├── Backend ├── Base de datos └── uploads/ ├── foto1.jpg ├── foto2.jpg └── foto3.jpg Quizá solamente existen unos cientos de archivos. No parece existir ningún problema. Ahora supongamos que la plataforma alcanza: 1,000,000 imágenes y cada imagen ocupa aproximadamente: 5 MB Estamos hablando de alrededor de: 5,000,000 MB ≈ 5 TB Y eso solamente considerando las imágenes. Agrega videos de cientos de megabytes, documentos, audios, archivos generados automáticamente y nuevos usuarios todos los días. El almacenamiento deja rápidamente de ser un detalle menor. 🚀 El verdadero problema aparece cuando agregas más servidores La capacidad del disco no es el único problema. Imagina que inicialmente tienes: Usuario ↓ Servidor A ↓ /uploads Todo funciona. Pero la aplicación crece y necesitas ejecutar tres instancias del backend: ┌→ Backend A Usuario → Balanceador → Backend B └→ Backend C Un usuario sube una imagen y su petición termina en Backend A . Si guardas el archivo únicamente en su disco: Backend A /uploads/foto.webp ✅ Backend B /uploads/foto.webp ❌ Backend C /uploads/foto.webp ❌ Después otra petición podría llegar al Backend C. Ahora aparece una pregunta incómoda: 👉 ¿Dónde está realmente la fotografía? Puedes diseñar almacenamiento compartido de otras maneras, por supuesto. Object Storage no es la única solución posible. Pero separar el almacenamiento persistente de las instancias de aplicación elimina esa dependencia del disco local. ☁️ Todos los backends pueden utilizar el mismo almacenamiento Con almacenamiento externo, el diseño cambia: ┌→ Backend A ─┐ Usuario ─────┼→ Backend B ─┼→ ☁️ Object Storage └→ Backend C ─┘ Ahora ninguna instancia necesita ser “la dueña” permanente del archivo. Los servidores de backend pueden incluso crearse, reiniciarse o eliminarse sin que eso implique perder las fotografías de los usuarios. Esta separación es especialmente importante en infraestructuras donde las instancias de aplicación pueden ser reemplazables. El backend procesa solicitudes. El almacenamiento mantiene los archivos persistentes. 🔑 ¿Cómo identificamos millones de objetos? Guardar millones de archivos también obliga a pensar correctamente en sus claves. Una opción podría parecer: foto.jpg Pero rápidamente aparecen conflictos. ¿Qué ocurre cuando 50,000 usuarios suben un archivo llamado exactamente foto.jpg ? Por eso las aplicaciones suelen generar claves que eviten colisiones. Por ejemplo: usuarios/847/550e8400-e29b-41d4-a716-446655440000.webp o una estructura similar a: perfiles/847/avatar-20260909.webp La estrategia exacta depende de la aplicación. Lo importante es no depender ciegamente del nombre original proporcionado por el usuario. 📂 Pero esas “carpetas” no funcionan exactamente como las de tu computadora En una interfaz de almacenamiento podrías ver: usuarios/ └── 847/ └── fotos/ └── verano.jpg Visualmente parece un sistema tradicional de carpetas. Pero muchos sistemas de Object Storage trabajan fundamentalmente con claves dentro de un espacio de nombres plano. Por ejemplo: usuarios/847/fotos/verano.jpg puede ser simplemente la clave completa del objeto. Los / permiten utilizar prefijos y presentar los objetos de una forma parecida a directorios. Conceptualmente podríamos tener: usuarios/847/fotos/verano.jpg usuarios/847/fotos/invierno.jpg usuarios/847/documentos/cv.pdf y buscar objetos que comiencen con: usuarios/847/fotos/ La interfaz puede mostrarlos como una carpeta, aunque internamente el modelo no tenga por qué ser idéntico a un sistema de archivos tradicional. 🔗 El archivo ni siquiera tiene que pasar por tu backend Ahora imagina que los usuarios pueden subir videos de: 500 MB El flujo más sencillo podría ser: Usuario ↓ Frontend ↓ Backend ↓ Object Storage Eso significa que los 500 MB atraviesan tu servidor de aplicación antes de llegar al almacenamiento. Si muchos usuarios hacen lo mismo simultáneamente, el backend termina utilizando ancho de banda y recursos simplemente para transportar archivos. Existe otra estrategia muy utilizada: las URLs firmadas o presigned URLs . 🔐 ¿Cómo funciona una URL firmada? El backend sigue participando en la autorización, pero no necesariamente transporta todo el archivo. Un flujo conceptual podría ser: 👤 Usuario ↓ ⚙️ Solicita permiso al backend ↓ 🔐 Backend genera autorización temporal ↓ 🌐 Frontend recibe URL firmada ↓ ☁️ Sube directamente al Object Storage Por ejemplo, el backend puede decidir: Este usuario tiene permiso para subir este objeto durante los próximos minutos. La aplicación recibe una URL temporal y utiliza algo parecido a: PUT https://storage.example/... Content-Type: image/webp para enviar directamente el archivo al servicio de almacenamiento. La URL puede expirar después de cierto tiempo. Así no necesitas hacer público todo el almacenamiento simplemente para permitir una carga. ⚠️ Una URL firmada no significa abandonar las validaciones Permitir cargas directas no significa: “Aquí tienes acceso al almacenamiento, sube cualquier cosa.” El backend todavía debería controlar aspectos importantes. Por ejemplo: ✔️ Qué usuario solicita la operación. ✔️ Qué tipo de archivo puede subir. ✔️ Qué tamaño máximo permites. ✔️ Qué objeto puede modificar. ✔️ Durante cuánto tiempo será válida la autorización. ✔️ Qué permisos tendrá posteriormente el archivo. Además, según el nivel de riesgo de la aplicación, los archivos pueden necesitar procesamiento adicional, análisis de seguridad o validaciones posteriores a la carga. La arquitectura cambia quién transporta los bytes. No elimina las reglas de seguridad. 🖼️ ¿Y qué ocurre después de subir una imagen? En aplicaciones reales, almacenar el archivo original puede ser solamente el comienzo. Imagina que alguien sube una fotografía de 8 MB y resolución enorme. Probablemente no quieras enviar esa imagen completa cada vez que necesites mostrar un avatar de 80×80 píxeles. Puedes tener un proceso como: 📸 Imagen original ↓ ☁️ Object Storage ↓ ⚙️ Procesamiento ↓ ├── perfil-80.webp ├── perfil-300.webp └── perfil-1200.webp El procesamiento puede ocurrir de forma independiente al servidor que atiende la petición original. Esto permite crear miniaturas, convertir formatos, comprimir archivos o generar diferentes versiones según las necesidades del producto. 🌍 Guardar el archivo y entregarlo son problemas diferentes Object Storage resuelve principalmente el almacenamiento. Pero supongamos que tienes usuarios en diferentes regiones solicitando millones de imágenes. Si todas las descargas tienen que llegar siempre directamente al mismo origen, puedes tener problemas de latencia y tráfico. Aquí puede entrar una CDN . El flujo conceptual cambia a: ☁️ Object Storage ↓ 🌍 CDN ↓ 👤 Usuarios La CDN puede mantener copias temporales del contenido en ubicaciones más cercanas a los usuarios. Así, un archivo solicitado frecuentemente no necesariamente necesita viajar desde el almacenamiento de origen en cada petición. Object Storage y CDN resuelven problemas relacionados, pero diferentes: Object Storage → ¿Dónde guardo el archivo? CDN → ¿Cómo lo distribuyo eficientemente? 🔒 No todos los archivos deberían ser públicos Una fotografía pública de un producto y un documento privado de un usuario tienen requisitos completamente diferentes. Por eso es importante definir correctamente los permisos. Puedes tener objetos públicos cuando realmente deban ser accesibles por cualquiera. Pero para contenido privado puede ser preferible mantener el almacenamiento cerrado y proporcionar acceso controlado. Por ejemplo: Usuario autenticado ↓ Backend verifica permisos ↓ ¿Puede descargar el documento? ↓ Sí ↓ URL temporal ↓ Descarga Así, conocer el nombre del archivo no necesariamente debería ser suficiente para acceder a él. 🗑️ También necesitas pensar en el ciclo de vida de los archivos Guardar objetos parece barato cuando solamente tienes unos cuantos gigabytes. El problema aparece cuando acumulas terabytes de archivos que nadie utiliza. Por ejemplo: Un usuario cambia su avatar diez veces. ¿Necesitas conservar para siempre las diez versiones anteriores? Una aplicación genera archivos temporales. ¿Quién los elimina? Creas copias de seguridad. ¿Cuánto tiempo deben conservarse? Un buen sistema necesita políticas de ciclo de vida. Conceptualmente: Archivo temporal ↓ 30 días sin utilizarse ↓ Eliminar automáticamente O: Backup reciente ↓ Después de cierto tiempo ↓ Mover a almacenamiento más económico Las posibilidades concretas dependen del proveedor y de las necesidades del proyecto. 💰 Object Storage también tiene costos que debes entender Mover los archivos fuera del servidor no significa que el almacenamiento sea gratuito o ilimitado. Dependiendo del proveedor puedes pagar por diferentes conceptos: 💾 Cantidad de información almacenada. 📥 Operaciones realizadas. 🌐 Transferencia de datos. 🔄 Recuperación de determinados tipos de almacenamiento. 📦 Características adicionales. Una arquitectura puede funcionar técnicamente muy bien y aun así resultar costosa si mueve enormes cantidades de información innecesariamente. Por eso también conviene pensar en compresión, formatos adecuados, caché, políticas de retención y patrones de acceso. ⚠️ Errores comunes al manejar archivos Uno de los más evidentes es depender permanentemente del disco local del backend en una arquitectura donde las instancias deberían ser reemplazables. Pero no es el único. También aparecen errores como: Guardar el nombre proporcionado por el usuario sin una estrategia segura para identificar objetos. Dejar todos los archivos públicos por comodidad. No limitar tamaños de carga. Confiar únicamente en la extensión: foto.jpg sin realizar las validaciones necesarias. Olvidar eliminar archivos que ya no tienen referencias. Guardar una nueva imagen en Object Storage, pero fallar antes de registrar correctamente su relación en la base de datos. O hacer lo contrario: Base de datos: foto = "usuarios/847/perfil.webp" Object Storage: ❌ El objeto ya no existe Cuando dos sistemas almacenan partes relacionadas de la información, también necesitas pensar en cómo mantenerlas consistentes. 🛠️ Buenas prácticas Cuando una aplicación comienza a manejar una cantidad considerable de archivos, algunas prácticas útiles son: ✔️ Separar el almacenamiento persistente del disco local de las instancias del backend cuando la arquitectura lo requiera. ✔️ Utilizar claves controladas por la aplicación y evitar depender únicamente del nombre original. ✔️ Guardar en la base de datos las referencias y metadatos que realmente necesites. ✔️ Definir límites de tamaño y formatos aceptados. ✔️ Mantener privados los objetos que no necesitan acceso público. ✔️ Utilizar URLs firmadas para cargas o descargas temporales cuando tenga sentido. ✔️ Procesar imágenes y videos en tamaños adecuados para su uso. ✔️ Establecer políticas para archivos temporales, antiguos o abandonados. ✔️ Utilizar una CDN cuando exista contenido público con mucho tráfico. ✔️ Monitorear no solamente cuánto almacenas, sino también cuánto transfieres. 🧩 Una arquitectura más realista Juntando varias de estas ideas, una aplicación podría terminar utilizando un flujo parecido a este: ┌─────────────────┐ │ Usuario │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Frontend │ └────────┬────────┘ │ solicita permiso ↓ ┌─────────────────┐ │ Backend │ └──────┬─────┬────┘ │ │ metadatos │ │ autorización ↓ ↓ ┌─────────┐ 🔐 │ Base de │ │ datos │ └─────────┘ Usuario ───── carga directa ───→ ☁️ Object Storage │ ↓ ⚙️ Procesamiento │ ↓ 🌍 CDN │ ↓ Usuarios No todas las aplicaciones necesitan esta arquitectura completa. Un proyecto pequeño puede funcionar perfectamente con algo mucho más sencillo. El objetivo no es utilizar servicios adicionales porque “así lo hacen las empresas grandes”. El objetivo es entender qué problema resuelve cada componente y agregarlo cuando realmente sea necesario. 🧩 La realidad Una aplicación puede manejar millones de imágenes sin que sus servidores de backend necesiten almacenar físicamente cada archivo en sus propios discos. La clave está en separar responsabilidades. ⚙️ Backend Lógica, permisos y reglas de negocio 🗄️ Base de datos Información estructurada y referencias ☁️ Object Storage Imágenes, videos, documentos y otros objetos ⚙️ Procesamiento Transformaciones cuando sean necesarias 🌍 CDN Distribución eficiente del contenido Object Storage no significa simplemente “tener un disco duro más grande en la nube”. Representa una forma diferente de diseñar el almacenamiento para que los archivos no estén atados al ciclo de vida de un servidor específico. Y cuando una aplicación empieza a crecer, esa diferencia se vuelve cada vez más importante. 💬 Escalar archivos también significa dejar de tratarlos como si toda tu aplicación siguiera viviendo dentro de una sola computadora. 👉 En tu último proyecto, ¿los archivos terminaron dentro de /uploads en el servidor o ya separaste el almacenamiento de tu backend? 🔥 El backend no se ve, pero sin él, nada funciona.

09 sep 2026
Leer publicación
Tu teclado utiliza un diseño creado cuando las computadoras ni siquiera existían
Curiosidad tecnológica

Tu teclado utiliza un diseño creado cuando las computadoras ni siquiera existían

Mira las primeras letras de tu teclado: Q — W — E — R — T — Y Ese orden tiene más de 150 años. Lo usamos para escribir mensajes, buscar en Internet, programar, redactar documentos y hasta controlar videojuegos. Está presente en laptops, teclados mecánicos y, de forma virtual, en nuestros teléfonos. Pero QWERTY no nació pensando en computadoras. Ni siquiera existían. Sus raíces están en las máquinas de escribir mecánicas del siglo XIX. Y eso plantea una pregunta bastante interesante: 👉 Si la tecnología ha cambiado tanto desde entonces, ¿por qué seguimos utilizando prácticamente la misma distribución? 🧠 QWERTY nació en la época de las máquinas de escribir Antes de poder abrir un editor de texto y borrar una palabra con Backspace , escribir era un proceso completamente mecánico. En las primeras máquinas de escribir, al presionar una tecla se activaba un mecanismo que movía una pieza metálica asociada a un carácter. Esta golpeaba una cinta con tinta contra el papel y dejaba impresa la letra. De forma muy simplificada: Presionas una tecla ↓ Se activa un mecanismo ↓ Una pieza metálica se mueve ↓ Golpea la cinta con tinta ↓ La letra queda impresa en el papel   No había pantalla. No había memoria. Y definitivamente no existía Ctrl + Z . Todo dependía de piezas físicas moviéndose rápidamente. ⚙️ El orden de las letras también era un problema mecánico Los primeros diseños de máquinas de escribir pasaron por diferentes distribuciones y mecanismos. Christopher Latham Sholes, junto con otros colaboradores, fue modificando la posición de las teclas durante el desarrollo de sus máquinas. La distribución no apareció de un día para otro exactamente como la conocemos ahora. Fue evolucionando. Las características mecánicas de aquellas máquinas influían en cómo convenía colocar determinadas letras. Dependiendo del diseño, ciertas secuencias podían hacer que los mecanismos asociados a las teclas interfirieran entre sí. De ese proceso terminaría surgiendo una distribución reconocible como antecesora del QWERTY moderno. Su nombre no tiene un significado técnico especial. Simplemente corresponde a las primeras seis letras de una de sus filas: Q W E R T Y   Algo que comenzó como parte del diseño de una máquina mecánica terminaría sobreviviendo muchísimo más que aquellas máquinas. 🤔 ¿QWERTY realmente fue creado para hacernos escribir más lento? Aquí aparece uno de los mitos tecnológicos más conocidos. Seguramente alguna vez has escuchado algo parecido a: “QWERTY fue diseñado para que las personas escribieran más lento y así evitar que las máquinas se atascaran.” Es una explicación atractiva porque parece tener sentido. Pero también simplifica demasiado una historia bastante más complicada. Las limitaciones mecánicas sí influyeron en la evolución de aquellas distribuciones. Sin embargo, eso no significa que el objetivo de QWERTY pueda resumirse simplemente como: Problema: las personas escriben demasiado rápido. Solución: acomodemos mal las letras para frenarlas.   Hubo diferentes versiones de máquinas, cambios en sus mecanismos, decisiones comerciales y ajustes en la distribución. Por eso es más correcto decir que QWERTY surgió dentro de un contexto donde las restricciones mecánicas importaban, en lugar de afirmar que fue diseñado exclusivamente para reducir la velocidad de escritura. 🚀 Pero ocurrió algo mucho más importante: la gente comenzó a aprenderlo Una tecnología cambia completamente cuando deja de ser solamente un producto y comienza a generar un ecosistema. Las máquinas de escribir con QWERTY se popularizaron. Las personas aprendieron dónde estaban las letras. Los mecanógrafos desarrollaron memoria muscular. Las empresas compraron máquinas compatibles. Las escuelas enseñaron a utilizarlas. Los fabricantes produjeron más equipos siguiendo distribuciones conocidas. Entonces comenzó a aparecer un fenómeno muy común en tecnología: 👉 Cuantas más personas utilizaban QWERTY, más conveniente resultaba continuar utilizando QWERTY. Y cuanto más conveniente era mantenerlo, más difícil se volvía reemplazarlo. 🔄 Las computadoras cambiaron el mecanismo, pero heredaron el teclado Con la llegada de las computadoras, muchas de las restricciones físicas de las antiguas máquinas de escribir dejaron de tener sentido. Una computadora moderna no necesita mover una barra metálica para escribir la letra A . Cuando presionas una tecla, el proceso es completamente diferente. De manera conceptual ocurre algo parecido a esto: Presionas una tecla ↓ El teclado detecta la pulsación ↓ Envía una señal al dispositivo ↓ El sistema operativo interpreta la entrada ↓ La aplicación recibe el carácter o evento ↓ La letra aparece en pantalla   El mecanismo original desapareció. Pero la distribución sobrevivió. ¿Por qué? Porque para ese momento el valor de QWERTY ya no dependía únicamente de cómo funcionaba una máquina. Dependía de que millones de personas sabían utilizarlo. 🧠 Aquí aparece algo llamado dependencia de trayectoria Existe una idea muy interesante que ayuda a entender fenómenos como este: la dependencia de trayectoria o path dependence . Simplificando bastante, significa que las decisiones tomadas anteriormente pueden limitar o influir en las opciones que resultan prácticas posteriormente. Imagina dos tecnologías: Tecnología A Muy utilizada Millones de usuarios Miles de productos compatibles Tecnología B Potencialmente más eficiente Pocos usuarios Menor ecosistema   No necesariamente gana B simplemente porque tenga algunas ventajas técnicas. Para cambiar de A hacia B hay que considerar todo lo que ya existe alrededor de A. Y ese costo puede ser enorme. 💻 Imagina reemplazar QWERTY actualmente Supongamos que mañana aparece una distribución nueva. Después de diferentes estudios se demuestra que, para determinados usuarios y tareas, puede resultar más cómoda o requerir menos movimiento de los dedos. Podrías pensar: “Perfecto. Entonces simplemente cambiemos todos los teclados.” Pero el problema real comienza ahí. Una persona acostumbrada durante años a escribir: console.log("Hola");   prácticamente no piensa conscientemente dónde están c , o , n , s , l y e . Sus dedos ya aprendieron esos movimientos. Cambiar la distribución significa romper temporalmente esa memoria muscular. Al principio escribirías más lento. Cometerías más errores. Buscarías las teclas. Tendrías que practicar nuevamente. Ahora multiplica ese problema por millones de personas. 🏢 No solamente tendrían que cambiar los usuarios El costo tampoco termina en aprender nuevamente a escribir. Hay todo un ecosistema alrededor de las distribuciones actuales: 👨‍💻 Desarrolladores acostumbrados a determinadas posiciones. 🏢 Empresas con miles de equipos. 🏫 Escuelas que enseñan mecanografía. ⌨️ Fabricantes de teclados. 💻 Fabricantes de laptops. 📱 Sistemas operativos con diferentes configuraciones. 🎮 Videojuegos con controles diseñados alrededor de ciertas posiciones físicas. ⌨️ Atajos de teclado utilizados durante décadas. Incluso tareas especializadas pueden depender de la posición de determinadas teclas. Así que reemplazar QWERTY no sería solamente cambiar unas letras de lugar. También significaría modificar hábitos, capacitación, hardware, configuraciones y flujos de trabajo. 🔄 ¿Entonces estamos atrapados para siempre con QWERTY? No necesariamente. De hecho, QWERTY no es la única distribución disponible. Existen alternativas conocidas como: Dvorak Fue diseñada décadas después de QWERTY y busca organizar las teclas teniendo en cuenta aspectos como la frecuencia de uso y el movimiento de los dedos. Colemak Es otra alternativa que busca mejorar comodidad y eficiencia, pero manteniendo algunas similitudes con QWERTY para que la transición no sea tan radical. Además, diferentes idiomas y países utilizan distribuciones distintas. Por ejemplo, existen variantes como QWERTZ y AZERTY, además de adaptaciones específicas según caracteres, acentos y necesidades lingüísticas. Esto demuestra algo importante: 👉 No existe una única distribución universal que necesariamente sea la mejor para todas las personas, idiomas y situaciones. ⌨️ ¿Una distribución diferente realmente puede hacerte escribir más rápido? Aquí también conviene evitar una conclusión demasiado sencilla. Cambiar de distribución no significa automáticamente duplicar tu velocidad de escritura. La velocidad depende de muchos factores: Experiencia. Práctica. Técnica. Memoria muscular. Idioma. Tipo de texto. Ergonomía. Características individuales. Algunas distribuciones alternativas pueden ofrecer ventajas relacionadas con comodidad o movimiento de los dedos para determinados usuarios. Pero existe una diferencia entre demostrar una ventaja bajo ciertas métricas y conseguir que millones de personas abandonen algo que llevan años utilizando. Y ese segundo problema suele ser mucho más difícil. ⚠️ Mejor tecnología ≠ tecnología ganadora QWERTY representa una lección que aparece constantemente en informática. Cuando evaluamos tecnologías solemos pensar solamente en características: ¿Cuál es más rápida? ¿Cuál consume menos recursos? ¿Cuál tiene mejor diseño? ¿Cuál es técnicamente más elegante?   Pero en el mundo real también existen otras variables: ✔️ Adopción. ✔️ Compatibilidad. ✔️ Costos de migración. ✔️ Conocimiento existente. ✔️ Herramientas disponibles. ✔️ Hábitos de los usuarios. ✔️ Ecosistema. ✔️ Soporte. Una solución puede ser técnicamente excelente y aun así no conseguir reemplazar a otra que ya está profundamente integrada en la vida de millones de personas. 🔗 Los efectos de red y los estándares importan muchísimo Cuando muchas personas utilizan algo, mantener compatibilidad con ellas puede convertirse en una ventaja por sí misma. Piensa en un teclado público, una computadora prestada o el equipo de una oficina. Si conoces QWERTY, probablemente puedas comenzar a utilizarlo inmediatamente. Esa familiaridad tiene valor. Un estándar reduce la cantidad de cosas que necesitamos volver a aprender cada vez que cambiamos de dispositivo. Por eso los estándares tecnológicos suelen ser difíciles de reemplazar. Una alternativa no solamente necesita ser mejor. Muchas veces necesita ser lo suficientemente mejor como para justificar todo el costo de abandonar lo anterior. 📱 Incluso las pantallas táctiles heredaron QWERTY Aquí está quizá la parte más curiosa. Un smartphone no tiene barras metálicas que puedan atascarse. Ni siquiera necesita tener teclas físicas. Su teclado es simplemente una interfaz dibujada en una pantalla. Técnicamente podríamos mostrar casi cualquier distribución. Sin embargo, desbloqueas tu teléfono y encuentras algo familiar: Q W E R T Y   Eso demuestra hasta qué punto una decisión tecnológica puede sobrevivir al problema que originalmente ayudó a resolver. Pasamos de: Máquina mecánica ↓ Máquina de escribir eléctrica ↓ Terminal ↓ Computadora personal ↓ Laptop ↓ Smartphone   El dispositivo cambió. La tecnología interna cambió. La forma en que procesamos texto cambió por completo. Pero el patrón aprendido por los usuarios continuó siendo útil. 🧩 La realidad Cada vez que escribes un mensaje, haces una búsqueda o programas, estás interactuando con una decisión tecnológica cuyas raíces vienen del siglo XIX. No porque una computadora moderna necesite QWERTY para funcionar. Podríamos cambiar la distribución mediante software con relativa facilidad en muchos dispositivos. El verdadero obstáculo está en otro lugar: 👉 Nosotros ya aprendimos a utilizarla. Y alrededor de ese aprendizaje existe un enorme ecosistema de hardware, software, documentación, educación y hábitos. QWERTY es un excelente recordatorio de que la evolución tecnológica no siempre consiste en reemplazar inmediatamente lo antiguo por algo técnicamente superior. A veces una tecnología permanece porque cambiarla cuesta más que seguir utilizándola. Y quizá la pregunta más interesante no sea por qué QWERTY sigue existiendo. Sino esta: 👉 Si mañana apareciera un teclado demostrablemente más cómodo y eficiente para ti, ¿sus ventajas serían suficientes para que estuvieras dispuesto a aprender a escribir nuevamente? 🔥 El backend no se ve, pero sin él, nada funciona.    

07 sep 2026
Leer publicación
¿Cómo sabe Spotify qué canción recomendarte después?
Arquitectura de Software

¿Cómo sabe Spotify qué canción recomendarte después?

🎧 Terminas una canción en Spotify… y empieza otra que parece elegida exactamente para ti No buscaste nada. No seleccionaste otra playlist. Simplemente terminó una canción y Spotify encontró otra que probablemente te guste. La escuchas completa. Después empieza otra que también encaja bastante bien. Y quizá unos minutos después aparece un artista que nunca habías escuchado, pero termina guardado entre tus favoritos. Entonces surge una pregunta interesante: 👉 ¿Cómo puede Spotify decidir qué reproducir después entre un catálogo enorme de música? No es magia ni significa que Spotify conozca exactamente lo que estás pensando. Detrás existen sistemas de recomendación capaces de analizar señales, encontrar relaciones y seleccionar contenido que tiene cierta probabilidad de interesarte. 🧠 Spotify no necesita preguntarte directamente qué te gusta Cuando utilizas una aplicación de música estás tomando pequeñas decisiones constantemente. Aunque no respondas un formulario diciendo: Me gusta este género. No me gusta este artista. Prefiero canciones tranquilas por la noche. tu comportamiento puede generar señales. Por ejemplo: ▶️ Reproduces una canción completa. ⏭️ Saltas otra rápidamente. ❤️ Guardas una canción. 🔁 Vuelves a escucharla varias veces. 🎵 Agregas una canción a una playlist. 🔎 Buscas determinado artista. 📀 Escuchas varias canciones de un mismo álbum. Una sola interacción normalmente no es suficiente para entender tus preferencias. Saltar una canción, por ejemplo, no significa necesariamente que la odies. Quizá simplemente no querías escucharla en ese momento. Pero cuando existen muchas interacciones acumuladas empiezan a aparecer patrones. 📊 Una señal aislada dice poco; muchas juntas dicen mucho más Imagina que escuchas una canción de rock una vez. Eso difícilmente permite concluir: “Este usuario ama el rock.” Pero supongamos que durante varias semanas ocurre esto: Escuchas Artista A completo ↓ Guardas canciones de Artista B ↓ Repites varias veces Artista C ↓ Creas una playlist con música similar ↓ Sueles saltar canciones de otros estilos Ahora existe bastante más contexto. El sistema puede comenzar a detectar que determinados artistas, canciones o características musicales aparecen frecuentemente en tus interacciones positivas. Y estas preferencias tampoco tienen por qué reducirse a una etiqueta simple como “rock” o “pop”. Una persona puede tener diferentes grupos de intereses que aparecen dependiendo del momento. ⚙️ ¿Cómo pasa Spotify de millones de canciones a unas pocas recomendaciones? Aquí aparece uno de los problemas más interesantes. Supongamos que una plataforma dispone de un catálogo enorme. Cuando termina tu canción, evaluar exhaustivamente cada canción disponible con modelos complejos para decidir cuál es la mejor sería costoso y poco práctico. Por eso, de manera conceptual, muchos sistemas de recomendación modernos separan el problema en diferentes etapas. Podemos imaginarlo así: Usuario ↓ Historial + interacciones + contexto ↓ Generación de candidatos ↓ Miles o cientos de opciones relevantes ↓ Ranking ↓ Selección final ↓ 🎵 Próximas recomendaciones La idea es reducir progresivamente el espacio de búsqueda. Primero encontrar opciones razonables. Después analizarlas con mayor detalle. 🔎 Primera etapa: encontrar canciones candidatas La generación de candidatos intenta responder: 👉 ¿Qué canciones vale la pena considerar para este usuario? En lugar de comparar todo el catálogo de la misma manera, pueden utilizarse diferentes fuentes o modelos para encontrar candidatos. Conceptualmente podrían aparecer canciones relacionadas con: Música que ya escuchaste. Artistas que te interesan. Contenido relacionado con tus preferencias. Patrones encontrados entre usuarios. Canciones similares o relacionadas. Música que todavía no conoces. Tendencias relevantes para determinado contexto. No significa que cada canción encontrada vaya a reproducirse. Simplemente entra al grupo de posibles recomendaciones. Es como hacer una primera selección. 🏆 Segunda etapa: ordenar las mejores opciones Después aparece otro problema. Supongamos que el sistema encontró 1,000 candidatos razonables. No puede reproducirlos todos al mismo tiempo. Necesita decidir cuáles colocar primero. Aquí entra el ranking. Un modelo de ranking puede utilizar diferentes señales para estimar qué tan apropiada podría ser cada opción. Simplificando mucho: Canción A → puntuación alta Canción B → puntuación media Canción C → puntuación baja Canción D → puntuación muy alta Después de evaluar candidatos, el sistema puede construir una selección mucho más pequeña y ordenada. Esto permite concentrar los cálculos más sofisticados en un conjunto reducido en lugar de procesar todo el catálogo con la misma profundidad. 👥 No solamente importa lo que tú escuchas Una de las ideas clásicas detrás de los sistemas de recomendación consiste en encontrar relaciones entre comportamientos. Imagina que escuchas frecuentemente: 🎵 Artista A 🎵 Artista B 🎵 Artista C Ahora supongamos que muchos usuarios que tienen patrones parecidos también escuchan: 🎵 Artista D Aunque tú nunca hayas reproducido al Artista D, aparece una relación interesante: Tú: A → B → C Usuarios con patrones similares: A → B → C → D Posible descubrimiento: D Esto se relaciona con técnicas conocidas como filtrado colaborativo. La idea general es aprovechar patrones colectivos para encontrar contenido que podría resultar relevante para alguien. Pero sería incorrecto pensar que una plataforma moderna depende exclusivamente de esta técnica. 🎵 Las propias canciones también pueden aportar información Existe otro enfoque: analizar relaciones o características del contenido. Imagina que disfrutas determinadas canciones y existen otras que presentan similitudes relevantes. El sistema podría aprovechar información asociada al contenido para encontrar nuevas opciones. Dependiendo del sistema, esto puede involucrar diferentes representaciones de canciones, artistas, géneros, metadatos u otras características obtenidas mediante modelos. Entonces aparecen dos perspectivas interesantes: ¿Qué consumen usuarios con patrones parecidos? + ¿Qué contenido está relacionado con aquello que te interesa? Combinar diferentes fuentes suele ser más útil que depender de una única señal. Este tipo de combinación se conoce generalmente como un enfoque híbrido. 🧮 ¿Cómo compara un sistema gustos y canciones? En sistemas modernos de machine learning es común representar elementos complejos mediante vectores numéricos conocidos como embeddings . Por ejemplo, conceptualmente podríamos representar una canción como: Canción A ↓ [0.81, 0.12, 0.47, 0.63, ...] Y otra: Canción B ↓ [0.78, 0.15, 0.51, 0.60, ...] Los números individualmente no tienen por qué tener un significado sencillo para nosotros. Lo interesante es que estas representaciones permiten realizar cálculos para encontrar elementos cercanos o relacionados dentro de un espacio matemático. También pueden construirse representaciones de usuarios o preferencias. Así, en lugar de preguntar literalmente: “¿A Herman le gusta esta canción?” un sistema puede trabajar con relaciones matemáticas entre representaciones aprendidas a partir de grandes cantidades de datos. Esto es una explicación conceptual: los sistemas reales pueden utilizar múltiples modelos, representaciones y etapas diferentes. 🎯 El contexto puede cambiar completamente una recomendación Aquí aparece otro detalle importante: Tus gustos no son una lista estática. Quizá escuches música energética cuando entrenas: 🏋️ Entrenamiento → música intensa. Pero mientras trabajas prefieres: 💻 Trabajo → música tranquila. Y por la noche: 🌙 Noche → otro tipo de contenido. Incluso aunque dos personas tengan gustos musicales similares, el contenido apropiado para cada momento puede ser diferente. Por eso un sistema de recomendación puede beneficiarse de diferentes señales contextuales y del comportamiento reciente, dependiendo del producto y de cómo esté diseñado. La pregunta deja de ser solamente: ¿Qué le gusta a este usuario? y puede convertirse en algo más parecido a: ¿Qué podría interesarle a este usuario en este contexto? 🔄 Tus gustos también cambian con el tiempo Imagina que hace seis meses descubriste un álbum nuevo. Durante varias semanas lo escuchaste constantemente. Si el sistema considerara todas tus interacciones históricas con exactamente el mismo peso, podría asumir que ese álbum continúa siendo una de tus prioridades para siempre. Pero quizá ya te cansaste de escucharlo. Tus interacciones recientes empiezan a mostrar otros intereses. Por eso los sistemas necesitan adaptarse. Conceptualmente: Preferencias históricas + Actividad reciente + Nuevas interacciones ↓ Representación actualizada ↓ Nuevas recomendaciones Esto convierte las recomendaciones en un problema dinámico. El usuario de hoy no necesariamente tiene exactamente las mismas preferencias que el usuario de hace un año. 🆕 ¿Y qué ocurre cuando eres un usuario nuevo? Aquí aparece un problema clásico de los sistemas de recomendación: el cold start o arranque en frío. Si acabas de crear una cuenta, existe muy poca información sobre ti. No hay meses de reproducciones. No existen cientos de canciones guardadas. Quizá todavía no creaste ninguna playlist. Entonces el sistema tiene menos señales personales para trabajar. Por eso algunas plataformas pueden solicitar preferencias iniciales o apoyarse más en información general, contenido popular, selecciones editoriales u otras señales mientras empiezan a observar tus interacciones. Conforme utilizas el servicio: Pocas señales ↓ Primeras reproducciones ↓ Búsquedas y skips ↓ Canciones guardadas ↓ Más historial ↓ Mayor información para personalizar No significa que cada recomendación vaya a ser perfecta, pero el sistema dispone de más evidencia para ajustar sus decisiones. ⚠️ “Spotify conoce perfectamente mis gustos” No exactamente. Esta es una diferencia importante. Un sistema de recomendación no necesita saber con absoluta certeza qué quieres escuchar. Trabaja con estimaciones. Podríamos imaginar algo como: Canción A → probablemente relevante Canción B → posiblemente relevante Canción C → menos probable Los modelos reales son mucho más complejos, pero la idea principal permanece: 👉 Se intenta estimar qué opciones tienen mayores posibilidades de producir una buena experiencia. Por eso Spotify puede recomendarte algo completamente inesperado. El modelo no leyó tu mente. Simplemente hizo una predicción que no coincidió contigo en ese momento. ⏭️ Incluso saltar una canción puede generar información Supongamos que aparece una recomendación y la saltas después de unos segundos. El sistema puede registrar esa interacción. Pero hay que tener cuidado con la interpretación. Un salto no necesariamente significa: ODIO ESTA CANCIÓN Podrías haberla saltado porque: No encajaba con ese momento. Ya la escuchaste demasiadas veces. Querías buscar otra canción. Estabas explorando rápidamente. Simplemente cambiaste de opinión. Por eso los sistemas suelen beneficiarse de observar conjuntos de señales y patrones, no de convertir una única acción en una verdad absoluta. 🔀 Existe otro reto: recomendar lo conocido o ayudarte a descubrir algo nuevo Imagina que Spotify descubre que te encantan cinco artistas. La estrategia más segura podría ser recomendar únicamente canciones de esos artistas. Probablemente acertaría muchas veces. Pero rápidamente aparecería otro problema: 👉 Todas tus recomendaciones comenzarían a sentirse iguales. Un sistema de recomendación útil necesita manejar un equilibrio conocido en machine learning y otros campos como exploration vs. exploitation . Simplificando: Explotación ↓ Recomendar cosas que probablemente ya sabemos que te gustan. Exploración ↓ Probar contenido diferente para descubrir nuevos intereses. Demasiada explotación puede producir recomendaciones repetitivas. Demasiada exploración puede provocar que muchas recomendaciones parezcan irrelevantes. Encontrar un equilibrio es parte del problema. 🎨 La diversidad también importa Supongamos que el ranking produce esto: 1. Canción del Artista A 2. Canción del Artista A 3. Canción del Artista A 4. Canción del Artista A 5. Canción del Artista A Quizá matemáticamente todas tengan puntuaciones altas. Pero como experiencia puede resultar aburrido. Por eso recomendar no consiste únicamente en ordenar elementos por una puntuación y terminar. Dependiendo del producto, también pueden importar objetivos como diversidad, descubrimiento, novedad o evitar demasiadas repeticiones. La mejor lista individual de canciones no necesariamente produce la mejor experiencia como conjunto. ⚡ Todo esto también es un problema de backend Hasta ahora parece principalmente un problema de machine learning. Pero existe otro desafío enorme: hacer que todo funcione a escala. Una plataforma de música necesita manejar enormes cantidades de información e interacciones. El sistema puede necesitar: ✔️ Registrar eventos generados por los usuarios. ✔️ Procesar información histórica. ✔️ Mantener representaciones y características actualizadas. ✔️ Obtener candidatos eficientemente. ✔️ Ejecutar modelos. ✔️ Construir rankings. ✔️ Servir recomendaciones rápidamente. Podemos imaginar una arquitectura conceptual: Aplicación ↓ Eventos de interacción ↓ Sistemas de procesamiento ↓ Datos / características / modelos ↓ Servicio de recomendaciones ↓ Generación de candidatos ↓ Ranking ↓ Aplicación La implementación real de Spotify es naturalmente mucho más compleja y evoluciona con el tiempo, pero este modelo ayuda a entender el tipo de componentes necesarios en un sistema de recomendación a gran escala. ⏱️ Una recomendación excelente que tarda demasiado también es un problema Imagina abrir una playlist y ver: Calculando recomendaciones... Tiempo estimado: 4 minutos. Sería una experiencia terrible. Los sistemas necesitan encontrar un equilibrio entre calidad y velocidad. Un modelo extremadamente sofisticado podría producir mejores predicciones, pero si requiere demasiado tiempo o demasiados recursos para cada petición quizá no sea práctico utilizarlo directamente en todas las etapas. Por eso resulta tan útil reducir candidatos progresivamente: Catálogo enorme ↓ Candidatos razonables ↓ Ranking más detallado ↓ Selección pequeña ↓ 🎧 Recomendaciones Cada etapa permite concentrar recursos donde aportan mayor valor. 🧩 La realidad Cuando Spotify reproduce una canción que parece perfecta para ese momento, detrás no necesariamente existe una regla sencilla como: IF usuario_escucha_rock: reproducir_rock() Los sistemas de recomendación modernos pueden combinar diferentes fuentes de información, modelos y objetivos. Analizan patrones. Generan candidatos. Estiman relevancia. Ordenan opciones. Intentan mantener variedad. Incorporan nuevas interacciones. Y todo debe ocurrir con suficiente rapidez para que, desde tu perspectiva, simplemente termine una canción… y comience la siguiente. La parte más interesante es que Spotify no necesita saber exactamente qué quieres escuchar. Necesita resolver una pregunta mucho más realista: 👉 Entre todas las canciones que podría mostrarte ahora, ¿cuáles tienen mejores señales para convertirse en tu próxima reproducción? Y algunas veces esa predicción termina presentándote una canción que ni siquiera sabías que estabas buscando. 💬 Una buena recomendación no necesita leer tu mente. Necesita encontrar algo con suficiente probabilidad de gustarte y, de vez en cuando, sorprenderte. 👉 ¿Cuántas canciones que hoy forman parte de tus favoritas llegaron a ti porque una aplicación decidió recomendártelas? 🔥 El backend no se ve, pero sin él, nada funciona.

06 sep 2026
Leer publicación
Tu aplicación está fallando en producción… pero en tu computadora todo funciona perfectamente.
DevOps

Tu aplicación está fallando en producción… pero en tu computadora todo funciona perfectamente.

Un usuario reporta: “La aplicación está muy lenta.” Otro dice: “Intenté hacer una compra y apareció un error.” Alguien más comenta: “A mí sí me funciona.” Revisas rápidamente el servidor. Está encendido. La base de datos responde. La CPU no parece estar al límite. Entonces aparece la gran pregunta: 👉 ¿Qué está ocurriendo realmente dentro del sistema? Porque saber que un servidor está “arriba” no significa que toda la aplicación esté funcionando correctamente. Puede existir un endpoint lento. Una consulta bloqueada. Una dependencia externa fallando. Un pool de conexiones saturado. Un error que solamente afecta a ciertos usuarios. Para entender todo eso necesitas algo más que revisar si el servidor sigue encendido. Necesitas observabilidad . 🧠 ¿Qué es la observabilidad? La observabilidad es la capacidad de entender qué está ocurriendo dentro de un sistema a partir de las señales que ese mismo sistema genera. En lugar de entrar a producción y comenzar a adivinar, puedes utilizar información real para reconstruir lo sucedido. Normalmente se habla de tres señales principales: 📜 Logs → qué ocurrió. 📊 Métricas → cómo se está comportando el sistema. 🔎 Trazas → por dónde pasó una petición y cuánto tardó en cada parte. Podemos imaginarlo así: Sistema ↓ Logs Métricas Trazas ↓ Información para investigar Cada una responde preguntas diferentes. 📜 Logs: qué ocurrió Los logs son registros de eventos generados por la aplicación. Por ejemplo: Usuario autenticado correctamente Pedido 1842 creado Timeout consultando servicio de pagos Error conectando a base de datos Cuando ocurre un problema, los logs pueden ayudarte a reconstruir qué estaba haciendo el sistema en ese momento. ⚠️ Un log sin contexto puede servir de poco Imagina encontrar: ERROR: Payment failed Sabes que falló un pago. Pero no sabes: ¿Qué pedido? ¿Qué usuario? ¿Qué proveedor? ¿En qué servidor? ¿A qué hora exactamente? Un log más útil podría incluir: level=ERROR request_id=abc123 order_id=1842 service=payments provider=stripe error=timeout Ahora tienes mucha más información para investigar. 📊 Métricas: cómo se está comportando el sistema Las métricas representan valores numéricos a lo largo del tiempo. Por ejemplo: CPU = 65% Requests por segundo = 1,200 Latencia promedio = 180 ms Errores 5xx = 2.3% Conexiones de base de datos = 95/100 Las métricas permiten detectar tendencias. Por ejemplo: Latencia 200 ms ↓ 300 ms ↓ 800 ms ↓ 2.5 s Algo claramente está empeorando. 🚀 Un ejemplo con una API lenta Supongamos que normalmente: GET /productos responde en: 120 ms Después de un deploy comienza a tardar: 1.8 segundos Una métrica puede mostrar: p95 latency: 120 ms → 1.8 s Ahora ya sabes que existe una degradación. Pero todavía falta responder: 👉 ¿Por qué? Ahí entran logs y trazas. 🔎 Trazas: por dónde pasó una petición Una traza permite seguir el recorrido de una petición a través del sistema. Por ejemplo: Usuario ↓ API Gateway ↓ Servicio de pedidos ↓ Servicio de pagos ↓ Base de datos Y además puede mostrar cuánto tardó cada paso. Por ejemplo: API Gateway 20 ms Pedidos 60 ms Base de datos 90 ms Pagos 3,700 ms Ahora el problema se vuelve evidente: 👉 El servicio de pagos está consumiendo casi todo el tiempo. 🧩 Una petición puede pasar por muchos sistemas En una arquitectura sencilla podrías tener: Frontend ↓ Backend ↓ Base de datos Pero en una plataforma más grande: Frontend ↓ API Gateway ↓ Pedidos ↓ Inventario ↓ Pagos ↓ Notificaciones Si la respuesta tarda cuatro segundos, necesitas saber exactamente dónde se gastaron esos cuatro segundos. Una traza distribuida ayuda a responder esa pregunta. 🆔 Request ID y Trace ID Para seguir una petición entre diferentes servicios se suelen utilizar identificadores. Por ejemplo: trace_id = 7f8a91... Ese mismo identificador puede viajar por: API ↓ Pedidos ↓ Pagos ↓ Inventario Y aparecer también en los logs. Entonces puedes buscar: trace_id=7f8a91 y reconstruir toda la operación. 🚀 Un ejemplo completo Supongamos que un usuario dice: “Intenté pagar y apareció un error.” Buscas el evento. Los logs muestran: request_id=abc123 order_id=1842 error=payment_timeout Después revisas la traza: POST /checkout ↓ Orders Service 50 ms ↓ Inventory Service 80 ms ↓ Payment Service 5,000 ms ↓ Timeout Y las métricas muestran: Payment Service Errores: 42% Latencia: 5 s Ahora tienes una historia completa. No solamente: "La compra falló." Sino: "Las compras están fallando porque el servicio de pagos está tardando más del timeout configurado." Eso cambia completamente la forma de investigar. ⚙️ ¿Qué diferencia existe entre monitoreo y observabilidad? Los conceptos están muy relacionados, pero no son exactamente lo mismo. El monitoreo suele partir de preguntas que ya conocemos. Por ejemplo: ¿Está encendido el servidor? ¿La CPU supera 90%? ¿La base de datos responde? ¿Hay demasiados errores 500? Creamos métricas y alertas para vigilar esas condiciones. 🧠 La observabilidad permite explorar preguntas nuevas Imagina que aparece un problema que nunca habías previsto: “Solo los usuarios que compran cierto producto están recibiendo errores.” Tal vez no tenías una alerta específica para eso. Pero si el sistema tiene buenas señales puedes investigar: ¿Qué tienen en común esas peticiones? ¿A qué endpoint llegan? ¿Qué servicio utilizan? ¿Qué consulta ejecutan? ¿En qué momento comenzaron los errores? La observabilidad permite investigar comportamientos inesperados utilizando la información disponible. 🚨 Saber que el servidor está arriba no es suficiente Supongamos que monitoreas: Servidor: UP ✅ Pero en realidad: Login: 200 ms ✅ Productos: 300 ms ✅ Checkout: 12 s ❌ Pagos: 40% errores ❌ Técnicamente el servidor sigue funcionando. Pero para parte de los usuarios la aplicación está rota. Por eso: UP no significa necesariamente: SALUDABLE 📈 Latencia promedio también puede engañar Imagina: Promedio = 200 ms Parece excelente. Pero quizá: 90% usuarios → 100 ms 10% usuarios → 1.1 s El promedio puede ocultar una mala experiencia para una parte importante de los usuarios. Por eso suelen observarse percentiles. Por ejemplo: p50 p90 p95 p99 🧠 ¿Qué significa p95? Si tienes: p95 = 800 ms significa, simplificando, que aproximadamente el 95% de las solicitudes fueron iguales o más rápidas que ese valor, mientras que el 5% fueron más lentas. Esto ayuda a entender la experiencia de los usuarios que están en la parte lenta de la distribución. Por ejemplo: Promedio = 180 ms p95 = 900 ms p99 = 4 s Hay solicitudes que claramente están teniendo problemas aunque el promedio se vea bien. 🚀 Un ejemplo después de un deploy Supongamos que despliegas: v2.4.0 a las: 14:00 A las 14:02 las métricas muestran: Errores 500 0.3% → 8% Y: p95 250 ms → 1.7 s Si tus dashboards marcan también el momento del deploy, puedes correlacionar: Deploy ↓ Errores aumentan Ahora tienes una pista muy fuerte. 🧩 Correlacionar deploys es muy útil Cuando ocurre un incidente, una de las primeras preguntas suele ser: “¿Qué cambió?” Puede ser: Nuevo deploy Migración Cambio de configuración Nueva dependencia Cambio de infraestructura Registrar esos eventos junto con las métricas reduce muchísimo el tiempo de investigación. 🗄️ Observabilidad de base de datos La aplicación puede estar funcionando correctamente mientras la base de datos comienza a degradarse. Por eso también conviene observar: Conexiones activas Consultas lentas Locks Deadlocks CPU I/O Replication lag Por ejemplo: Pool: 100 conexiones disponibles En uso: 99 Eso puede explicar por qué nuevas peticiones empiezan a esperar. ⚠️ Saturación del pool de conexiones Imagina: Backend ↓ Pool de 100 conexiones ↓ Base de datos Si las 100 están ocupadas: Request nueva ↓ Esperar conexión Entonces puedes observar: Consulta SQL: 50 ms pero la petición completa tarda: 2 segundos porque pasó 1.95 segundos esperando una conexión. Sin buenas métricas podrías culpar erróneamente a la consulta. 📡 También debes observar dependencias externas Tu aplicación puede estar perfecta. Pero depende de: Pagos Email Maps Storage APIs externas Si uno empieza a fallar, tu sistema también puede degradarse. Conviene medir: Latencia por proveedor Tasa de errores Timeouts Retries Circuit breakers Por ejemplo: Proveedor de pagos ↓ p95 = 4.2 s puede explicar inmediatamente un checkout lento. 🔄 Los reintentos pueden ocultar problemas Supongamos: Intento 1 → error Intento 2 → error Intento 3 → éxito El usuario quizá recibe una respuesta correcta. Pero la operación tardó mucho más. Si únicamente mides: Resultado final = exitoso puedes perder información. También conviene observar: Cantidad de retries Errores iniciales Tiempo total Una tasa de reintentos creciente puede indicar que una dependencia está degradándose antes de que falle completamente. 🚨 Las alertas deben indicar problemas útiles Puedes crear una alerta para: CPU > 50% Pero quizá tu aplicación funciona perfectamente con 75%. Entonces recibirás alertas constantemente. Con el tiempo el equipo comienza a ignorarlas. Esto se conoce como alert fatigue . Una alerta debería representar algo que realmente necesite atención. 🎯 Alertar sobre síntomas importantes En muchos casos puede ser más útil alertar sobre: Errores 5xx > 5% p95 > 2 segundos Checkout fallando Cola creciendo durante 10 minutos que sobre cualquier pequeño cambio técnico. La infraestructura importa. Pero también importa lo que experimentan los usuarios. 🧠 Métricas técnicas y métricas de negocio Una aplicación puede tener: CPU RAM Latencia Requests pero también métricas del negocio: Compras completadas Pagos rechazados Usuarios registrados Pedidos creados Imagina: CPU normal ✅ RAM normal ✅ Errores HTTP normales ✅ pero: Compras completadas = 0 durante una hora. Probablemente existe un problema importante. Por eso observabilidad también puede incluir señales cercanas al funcionamiento real del producto. 🚀 Un ejemplo de métrica de negocio Normalmente tienes: 200 compras por hora Después de un deploy: 5 compras por hora Puede existir un error lógico que no genera respuestas 500. Quizá el botón funciona. La API responde. Pero el flujo de compra quedó roto. Una métrica de negocio puede detectar algo que las métricas tradicionales no muestran. 📜 Logs estructurados En lugar de escribir: "Hubo un error con el pedido" puede ser mejor generar información estructurada. Por ejemplo: { "level": "error", "service": "orders", "order_id": 1842, "request_id": "abc123", "error": "payment_timeout" } Esto facilita buscar y filtrar. Por ejemplo: Todos los errores donde service=orders y error=payment_timeout Los logs dejan de ser simples líneas de texto y se vuelven información consultable. ⚠️ Pero tampoco debes registrar todo Un error común es pensar: “Mientras más logs, mejor.” No necesariamente. Si generas: millones de líneas irrelevantes puede ser más difícil encontrar lo importante. Además aumentan: Costos Almacenamiento Ruido Conviene registrar información útil y con niveles adecuados. 📋 Niveles de logs Es común encontrar niveles como: DEBUG INFO WARN ERROR Por ejemplo: DEBUG Información detallada de desarrollo INFO Evento normal importante WARN Situación inesperada pero recuperable ERROR Operación fallida No todo debería convertirse en ERROR . 🔐 Cuidado con información sensible Nunca deberías registrar sin cuidado: Contraseñas Tokens Datos de tarjetas Secretos Información personal sensible Un sistema de logs puede ser consultado por múltiples personas y almacenarse durante mucho tiempo. Por eso la observabilidad también necesita una política de seguridad. 🔎 Trazas distribuidas Cuando tienes varios servicios: Gateway ↓ Orders ↓ Payments ↓ Database cada operación puede generar un span . Conceptualmente: Trace: Checkout ├── API Gateway 20 ms ├── Orders 100 ms ├── Payments 2,500 ms └── Database 80 ms Cada span representa una parte del trabajo. Esto permite visualizar dónde se consume el tiempo. 🧩 No necesitas microservicios para utilizar trazas Aunque son especialmente útiles en sistemas distribuidos, también pueden ayudar en aplicaciones monolíticas. Por ejemplo: Request ↓ Controller ↓ Service ↓ ORM ↓ Query puedes medir cuánto tarda cada etapa. La observabilidad no es exclusiva de arquitecturas gigantes. 📊 Dashboards Las métricas pueden mostrarse en dashboards. Por ejemplo: Requests/s Errores Latencia CPU RAM DB connections Esto permite observar rápidamente el estado del sistema. Pero un dashboard debería responder preguntas útiles. No convertirse en una pantalla con 200 gráficas que nadie revisa. 🚨 Health checks Las aplicaciones también pueden exponer endpoints como: /health Por ejemplo: { "status": "ok" } Un balanceador puede utilizarlos para saber si una instancia está disponible. Pero un buen health check debe diseñarse con cuidado. ⚠️ Un health check puede dar falsa seguridad Si: GET /health ↓ 200 OK pero: Base de datos caída quizá realmente la aplicación no pueda servir tráfico útil. Entonces puede tener sentido distinguir entre: Liveness y: Readiness 🟢 Liveness Pregunta: ¿El proceso sigue vivo? Si no: reiniciar 🔵 Readiness Pregunta: ¿Está realmente preparado para recibir tráfico? Por ejemplo: Proceso vivo ✅ Base de datos disponible ✅ Configuración cargada ✅ Entonces: Ready Esto permite tomar mejores decisiones operativas. 🚀 OpenTelemetry En observabilidad moderna existe un estándar muy conocido llamado OpenTelemetry . Permite instrumentar aplicaciones para generar y transportar señales como: Métricas Trazas Logs hacia diferentes plataformas de observabilidad. La idea es evitar que toda la instrumentación quede completamente acoplada a una herramienta específica. 🛠️ Herramientas comunes Dependiendo de la arquitectura puedes encontrar tecnologías como: Prometheus Grafana ELK / Elastic Loki Jaeger Tempo Datadog New Relic Sentry Cada una puede ayudar con diferentes partes. Pero tener muchas herramientas no significa automáticamente tener buena observabilidad. 🧠 La observabilidad empieza en el diseño Puedes instalar diez plataformas. Pero si tu aplicación solo genera: Something went wrong seguirás teniendo poca información. Una buena observabilidad requiere decidir: ¿Qué eventos registrar? ¿Qué métricas importan? ¿Qué identificadores propagar? ¿Qué errores diferenciar? La herramienta viene después. 🚀 Un incidente con buena observabilidad Usuario reporta: “Mi pago falló.” Buscas su petición. Encuentras: request_id = abc123 La traza muestra: Checkout ↓ Payments ↓ Timeout Los logs muestran: provider_timeout Las métricas muestran: Errores de pagos aumentaron desde las 14:03 Y el dashboard marca: Deploy v2.4 14:02 Ahora la investigación tiene una dirección clara. ❌ El mismo incidente sin observabilidad Usuario: “Mi pago falló.” Equipo: ¿Será la base de datos? ¿Será el proveedor? ¿Será la red? ¿Será el frontend? ¿Reiniciamos? Ahí aparece la diferencia. Observabilidad no evita necesariamente el incidente. Reduce cuánto tiempo tardas en entenderlo. ⏱️ MTTR Una métrica importante en operaciones es: Mean Time To Recovery o Mean Time To Repair según el contexto. De forma general, representa cuánto tardamos en recuperarnos de un problema. Una buena observabilidad puede reducirlo porque disminuye el tiempo perdido intentando descubrir dónde está la falla. ⚠️ El tiempo de diagnóstico importa muchísimo Imagina: Incidente dura 30 minutos pero: 25 minutos ↓ Encontrar causa 5 minutos ↓ Arreglarla La mayor parte del incidente se perdió investigando. Si mejores señales permiten localizar el problema en cinco minutos, el impacto puede reducirse drásticamente. 📱 El frontend también necesita observabilidad No todo error ocurre en el backend. Puede haber: JavaScript error API cancelada Componente roto Problema de navegador Error de renderizado que solamente aparecen del lado del usuario. Herramientas frontend pueden capturar: Stack trace Navegador Versión Ruta Errores JS Esto ayuda a investigar problemas que el servidor nunca llega a ver. 🌐 Ejemplo Tu API devuelve: 200 OK Perfectamente. Pero el frontend recibe: { "price": null } y una función JavaScript no esperaba null . Resultado: Interfaz rota Desde backend todo parece funcionar. La observabilidad frontend permite descubrir la otra mitad de la historia. 🧪 Staging también debería tener observabilidad La observabilidad no sirve únicamente en producción. En staging puedes detectar antes: Consultas lentas Errores silenciosos Timeouts Problemas de integración antes del deploy. Eso ayuda a que las señales operativas formen parte de las pruebas. 🔄 Observabilidad y despliegues Cada despliegue debería poder relacionarse con métricas. Por ejemplo: v2.3 ↓ Latencia estable Después: Deploy v2.4 ↓ Latencia aumenta Esa correlación puede acelerar muchísimo una decisión de rollback. 🚨 No esperes a que un usuario reporte el problema Un buen sistema de alertas debería permitir detectar problemas antes. Por ejemplo: Error rate > 5% genera una alerta. El equipo investiga. Quizá todavía ningún usuario abrió un ticket. Ese es uno de los grandes beneficios de tener señales adecuadas. 🧠 Observabilidad también ayuda cuando todo funciona No solamente sirve para incidentes. Puedes utilizarla para responder: ¿Qué endpoint consume más recursos? ¿Qué consulta es más lenta? ¿Qué función se utiliza más? ¿Dónde existen cuellos de botella? Eso ayuda a mejorar rendimiento y capacidad antes de que exista una falla. 📈 Capacity Planning Si observas: DB connections 50% ↓ 60% ↓ 75% ↓ 85% durante varios meses, puedes anticipar que pronto necesitarás ajustar la infraestructura. Sin métricas, probablemente descubrirías el límite cuando el sistema ya esté saturado. ⚠️ Observabilidad también tiene costo Guardar: Cada log Cada trace Cada métrica de millones de solicitudes puede ser costoso. Por eso pueden utilizarse estrategias como: Retención limitada Sampling Agregación Filtrado No necesitas guardar absolutamente todo para siempre. 🎯 Sampling de trazas Supongamos que recibes: 1,000,000 requests No siempre necesitas almacenar una traza completa de cada una. Puedes guardar una muestra. Por ejemplo: 5% Y quizá conservar: 100% de las trazas con error La estrategia depende del sistema. 🔍 Pero no muestrees a ciegas Si el problema afecta: 0.1% de usuarios y tu sampling es demasiado agresivo, podrías perder casi todos los ejemplos relevantes. Por eso el muestreo también necesita diseñarse con cuidado. 🛠️ Buenas prácticas ✔️ Centraliza los logs para no buscarlos servidor por servidor. ✔️ Utiliza logs estructurados y con contexto. ✔️ Propaga request_id o trace_id entre componentes. ✔️ Mide latencia, errores, tráfico y saturación. ✔️ Utiliza percentiles además de promedios. ✔️ Observa también dependencias externas y bases de datos. ✔️ Configura alertas que representen problemas reales. ✔️ Relaciona deploys con cambios de comportamiento. ✔️ Nunca registres secretos o información sensible sin control. ✔️ Diseña la observabilidad antes de necesitarla durante un incidente. 🧩 La realidad En producción no puedes depender de: “A mí sí me funciona.” Ni de: “El servidor está encendido.” Una aplicación puede estar disponible y aun así estar funcionando mal para miles de usuarios. Necesitas señales que te permitan responder: ¿Qué pasó? ¿Cuándo comenzó? ¿A quién afecta? ¿Dónde ocurrió? ¿Qué cambió? ¿Por qué está tardando? Ahí es donde logs, métricas y trazas trabajan juntos. 💬 Un sistema observable no es uno que nunca falla. Es uno que deja suficientes señales para que, cuando falle, puedas entender qué ocurrió sin tener que investigar completamente a ciegas. 👉 La próxima vez que alguien diga: “La aplicación está lenta.” la respuesta ideal no debería ser: “Voy a revisar a ver qué encuentro.” Debería ser posible seguir las señales del sistema hasta encontrar exactamente dónde comenzó el problema. 🔥 El backend no se ve, pero sin él, nada funciona.

31 ago 2026
Leer publicación
Una migración puede funcionar perfectamente… y aun así romper tu aplicación en producción.
Base de datos

Una migración puede funcionar perfectamente… y aun así romper tu aplicación en producción.

Necesitas modificar la base de datos. Agregas una columna. Renombras otra. Eliminas una que ya no utilizas. Ejecutas la migración. Todo termina correctamente. No hubo errores. La base de datos aceptó el cambio. Entonces parece que todo salió bien. Pero segundos después... 💥 La aplicación empieza a devolver errores. Los usuarios no pueden completar ciertas acciones. Algunos endpoints comienzan a fallar. Y aparecen mensajes relacionados con columnas que ya no existen. La migración técnicamente funcionó. El problema es que: 👉 el código que todavía estaba ejecutándose esperaba la estructura anterior de la base de datos. Eso es lo peligroso de una migración incompatible . 🧠 ¿Qué es una migración incompatible? Una migración incompatible es un cambio en la estructura de la base de datos que deja de funcionar correctamente con alguna versión de la aplicación que todavía está activa. Por ejemplo, imagina que tu código utiliza: usuarios.nombre Pero decides renombrar directamente esa columna a: usuarios.nombre_completo La migración puede ejecutarse perfectamente: nombre ↓ nombre_completo La base de datos no tiene ningún problema con eso. Pero el código anterior todavía contiene consultas como: SELECT nombre FROM usuarios; Ahora esa columna ya no existe. Resultado: Código anterior ↓ Busca usuarios.nombre ↓ ❌ Columna inexistente La base de datos quedó correctamente migrada. Pero la aplicación quedó temporalmente incompatible con ella. ⚙️ ¿Por qué esto ocurre principalmente en producción? Porque un despliegue real no siempre sucede en un único instante. Imagina que tienes varios servidores: Servidor 1 Servidor 2 Servidor 3 Servidor 4 Están detrás de un balanceador de carga. Cuando haces deploy, quizá los servidores se actualicen uno por uno. Durante unos segundos podrías tener: Servidor 1 → versión nueva Servidor 2 → versión nueva Servidor 3 → versión anterior Servidor 4 → versión anterior Pero todos siguen utilizando: La misma base de datos Ahí aparece el problema. La estructura de la base debe ser entendida temporalmente por ambas versiones. 🚀 Un ejemplo paso a paso Supongamos que tenemos: usuarios ---------------- id nombre email Y queremos cambiar: nombre por: nombre_completo Una estrategia peligrosa sería hacer directamente: ALTER TABLE usuarios RENAME COLUMN nombre TO nombre_completo; Después desplegar el nuevo código. Durante ese pequeño intervalo: Base de datos: nombre_completo ✅ nombre ❌ Código viejo: busca nombre ❌ Código nuevo: busca nombre_completo ✅ Las peticiones que lleguen al servidor antiguo podrían fallar. 🧩 El problema no siempre dura mucho Tal vez solamente exista durante: 30 segundos Pero si tu aplicación recibe: 5,000 solicitudes por segundo 30 segundos pueden representar muchísimos errores. Una incompatibilidad breve puede afectar a miles de usuarios. Por eso en sistemas de producción importa mucho pensar en la transición, no solamente en el estado final. 🔄 Una estrategia más segura: expandir y después contraer Una estrategia común consiste en hacer los cambios en varias etapas. A veces se conoce como: Expand and Contract La idea es: Primero agregar lo nuevo ↓ Después adaptar el código ↓ Finalmente eliminar lo viejo No destruir la estructura anterior inmediatamente. 1️⃣ Agregar la nueva columna Primero mantenemos: nombre y agregamos: nombre_completo Ahora tenemos: usuarios ---------------- id nombre nombre_completo email El código anterior continúa funcionando porque: nombre todavía existe. 2️⃣ Hacer el código compatible Después desplegamos una versión capaz de trabajar con la nueva estructura. Por ejemplo, temporalmente puede escribir en ambas columnas: nombre = "Herman Primo" nombre_completo = "Herman Primo" O leer primero la nueva y utilizar la anterior como respaldo. Conceptualmente: ¿Existe nombre_completo? ├── Sí → utilizarlo └── No → utilizar nombre La implementación exacta depende del cambio. Lo importante es que durante la transición el código pueda convivir con ambas estructuras. 3️⃣ Migrar los datos existentes Ahora necesitamos copiar los valores antiguos. Por ejemplo: UPDATE usuarios SET nombre_completo = nombre WHERE nombre_completo IS NULL; Entonces los registros existentes pasan de: nombre = Herman Primo nombre_completo = NULL a: nombre = Herman Primo nombre_completo = Herman Primo Ahora ambas columnas contienen la información. 4️⃣ Cambiar completamente el código Después desplegamos una versión que utiliza únicamente: nombre_completo Por ejemplo: Versión nueva ↓ Lee nombre_completo ↓ Escribe nombre_completo Pero todavía no eliminamos: nombre inmediatamente. 5️⃣ Confirmar que nadie utiliza la columna antigua Antes de eliminarla conviene comprobar: ¿Algún servidor antiguo sigue activo? ¿Algún worker utiliza nombre? ¿Algún script utiliza nombre? ¿Algún reporte depende de esa columna? Este punto es fácil de olvidar. Quizá el backend principal ya fue actualizado. Pero existe: Worker antiguo Cron job Script de administración Proceso ETL que todavía depende de: usuarios.nombre Eliminarla demasiado pronto volvería a romper el sistema. 6️⃣ Finalmente eliminar lo viejo Solo después de confirmar que ya no existe ninguna dependencia podemos ejecutar: ALTER TABLE usuarios DROP COLUMN nombre; Ahora sí: usuarios ---------------- id nombre_completo email La transición terminó. 🧠 La regla general Podemos resumirlo así: Agregar ↓ Compatibilizar ↓ Migrar ↓ Desplegar ↓ Verificar ↓ Eliminar En lugar de: Eliminar primero ↓ Esperar que todo lo demás funcione Esta forma de trabajar reduce mucho el riesgo. ⚠️ Renombrar columnas no es el único cambio peligroso También existen otros tipos de migraciones incompatibles. Por ejemplo: Eliminar columnas Cambiar tipos de datos Agregar restricciones Modificar valores permitidos Cambiar relaciones Todos pueden romper versiones anteriores del código. 💥 Eliminar una columna Imagina que tienes: usuarios.telefono El nuevo código ya no la utiliza. Entonces decides eliminarla. Pero todavía hay un servidor antiguo ejecutando: SELECT id, nombre, telefono FROM usuarios; Ahora ese endpoint falla. Aunque la columna ya no fuera necesaria para la versión nueva, seguía siendo necesaria para la anterior. 🔢 Cambiar un tipo de dato Supongamos: pedido.estado era: VARCHAR Y decides convertirlo en un entero. Antes: "PENDIENTE" "PAGADO" "CANCELADO" Después: 1 2 3 El nuevo código sabe interpretar esos números. Pero el código anterior espera strings. Ahora podemos tener: Base de datos → 2 Código viejo → espera "PAGADO" La estructura sigue siendo válida para la base. Pero no necesariamente para todos los consumidores. 🚧 Agregar una restricción también puede romper el deploy Supongamos que tienes: email permitiendo: NULL Y quieres convertirlo en: NOT NULL Parece sencillo: ALTER TABLE usuarios ALTER COLUMN email SET NOT NULL; Pero si existen registros antiguos: email = NULL la migración puede fallar. Incluso si consigues ejecutarla, una versión anterior del código todavía podría intentar crear usuarios sin correo. Entonces: Código viejo ↓ INSERT sin email ↓ ❌ Restricción NOT NULL Otra incompatibilidad. ✅ Una forma más segura de agregar NOT NULL Primero: 1. Asegurar que el nuevo código siempre envíe email Después: 2. Completar registros antiguos Luego: 3. Confirmar que no existen NULL Y finalmente: 4. Agregar NOT NULL Otra vez: 👉 Primero haces que el sistema sea compatible. Después aplicas la restricción definitiva. 🗄️ Las migraciones de datos también necesitan cuidado No todas las migraciones modifican únicamente estructura. Algunas necesitan transformar información. Por ejemplo: nombre = Herman apellido = Primo y quieres crear: nombre_completo = Herman Primo Quizá tengas: 10 millones de usuarios Ejecutar: UPDATE usuarios SET nombre_completo = ... sobre todos los registros en una sola operación podría generar: Mucha carga. Locks prolongados. Crecimiento del log de transacciones. Replication lag. Problemas de rendimiento. 📦 Migrar datos por lotes En esos casos puede ser mejor procesar información gradualmente. Por ejemplo: 10,000 registros ↓ 10,000 registros ↓ 10,000 registros ↓ ... En lugar de intentar modificar millones de filas en una sola transacción. Esto se conoce normalmente como procesamiento por lotes o batching . Ayuda a controlar el impacto sobre producción. 🔒 Las migraciones también pueden bloquear tablas Dependiendo del motor y del cambio realizado, una operación como: ALTER TABLE puede necesitar adquirir locks. Eso puede impedir temporalmente: INSERT UPDATE DELETE o incluso ciertas lecturas. Entonces una migración que tarda: 5 minutos podría afectar a usuarios durante todo ese tiempo. ⚡ Una migración rápida en desarrollo puede ser lenta en producción En local tienes: 500 filas La migración tarda: 0.2 segundos Producción tiene: 50 millones de filas La misma operación puede tardar muchísimo más. Por eso no basta con comprobar: La migración funciona También necesitas preguntar: ¿Cuánto tarda con datos reales? ¿Bloquea? ¿Cuánto CPU consume? ¿Cuánto almacenamiento temporal necesita? 🚀 Aquí staging se vuelve muy importante Antes de ejecutar una migración crítica en producción, conviene probarla en un entorno parecido. Por ejemplo: Staging ↓ Datos representativos ↓ Ejecutar migración ↓ Medir tiempo ↓ Revisar errores Esto puede revelar: Consultas lentas. Datos incompatibles. Falta de espacio. Problemas de configuración. Locks inesperados. Es mucho mejor descubrirlos ahí. 🔄 Rolling Deploys Muchas plataformas realizan despliegues progresivos. Por ejemplo: Versión vieja Versión vieja Versión vieja Después: Versión nueva Versión vieja Versión vieja Luego: Versión nueva Versión nueva Versión vieja Finalmente: Versión nueva Versión nueva Versión nueva Este enfoque reduce interrupciones. Pero obliga a pensar mucho más en compatibilidad. Durante un periodo, ambas versiones comparten la misma base de datos. 🟢 Zero-Downtime Deployments El objetivo de muchas estrategias modernas es desplegar sin detener completamente la aplicación. Esto se conoce como: Zero-Downtime Deployment Para lograrlo, la base de datos también debe evolucionar de forma compatible. No sirve tener servidores actualizándose perfectamente si en medio del proceso ejecutas una migración que rompe inmediatamente todas las versiones anteriores. ⚠️ El orden del deploy importa Supongamos que agregas: usuarios.nombre_completo Una secuencia segura podría ser: 1. Migración compatible 2. Deploy nuevo código 3. Migración de datos 4. Eliminar estructura antigua posteriormente Pero si haces: 1. Eliminar nombre 2. Deploy código nuevo existe un intervalo donde el sistema queda roto. El orden de los pasos forma parte de la arquitectura del despliegue. 🧩 Código y esquema deben evolucionar juntos La aplicación no existe aislada. Tenemos: Código + Base de datos Una versión del código espera un determinado esquema. Por ejemplo: App v1 ↓ Schema v1 Después: App v2 ↓ Schema v2 El problema aparece durante: App v1 + Schema v2 o: App v2 + Schema v1 si no fueron diseñados para convivir. Por eso las transiciones importan. 🔄 Compatibilidad hacia atrás Una migración compatible hacia atrás permite que el nuevo esquema continúe funcionando temporalmente con el código anterior. Por ejemplo: Schema viejo: nombre Nuevo esquema temporal: nombre nombre_completo El código antiguo sigue encontrando: nombre mientras el nuevo ya puede comenzar a utilizar: nombre_completo Esto facilita muchísimo los despliegues progresivos. 📡 No olvides workers y procesos en segundo plano Imagina: Backend actualizado ✅ Pero tienes un worker ejecutando desde hace una hora: Worker antiguo ↓ SELECT usuarios.nombre Eliminas: nombre Y el worker empieza a fallar. Por eso antes de hacer cambios destructivos también hay que considerar: Workers. Cron jobs. Consumers. Scripts. Procesos batch. Herramientas administrativas. No solamente servidores HTTP. 🔌 También pueden existir consumidores externos Quizá tu propia aplicación ya no utilice una columna. Pero: Sistema de reportes Sistema BI Integración externa ETL podría seguir dependiendo de ella. Una modificación de base de datos puede afectar más sistemas de los que parece. Por eso conviene entender las dependencias antes de destruir una estructura. 🧹 Cambios destructivos deberían llegar al final Eliminar algo debería ser la última etapa. Por ejemplo: Agregar nueva estructura ↓ Adoptarla ↓ Migrar información ↓ Observar ↓ Confirmar que lo viejo no se usa ↓ Eliminar De esta forma, si algo falla durante las primeras etapas, todavía existe la estructura anterior como respaldo temporal. ⚠️ Error común: migración y deploy como una única operación indivisible Imagina un script: git pull ↓ migrate ↓ restart Puede funcionar en proyectos pequeños. Pero conforme aumenta la complejidad, quizá necesites separar: Cambios compatibles ↓ Deploy ↓ Migraciones adicionales ↓ Limpieza posterior No todas las modificaciones deberían ocurrir automáticamente en el mismo instante. 🔙 ¿Y el rollback? Aquí aparece otro problema importante. Supongamos: Deploy v2 ↓ Migración elimina columna Después descubres un error grave y decides: Rollback a v1 Pero v1 necesita la columna que ya eliminaste. Entonces: Código v1 ↓ Busca columna antigua ↓ ❌ Ya no existe Tu rollback dejó de ser posible. 🧠 Las migraciones destructivas complican los rollbacks Por eso una estrategia segura puede mantener temporalmente la estructura antigua. Si necesitas volver de: v2 a: v1 la base todavía puede soportarla. Esto convierte el rollback en una opción real y no solamente en una idea. 🧩 Rollback de código y rollback de datos no son lo mismo Volver a una imagen Docker anterior puede ser sencillo. Pero revertir: DELETE COLUMN no recupera automáticamente los datos que existían dentro. Por ejemplo: DROP COLUMN telefono Después haces: ADD COLUMN telefono La columna regresa. Pero los datos originales no. Por eso los cambios destructivos deben tratarse con mucho cuidado. 💾 Backups siguen siendo importantes Antes de migraciones críticas puede ser necesario tener: Backup Snapshot Punto de recuperación Pero tampoco significa que el backup sea tu estrategia principal. Restaurar una base completa puede tardar y afectar a usuarios. El objetivo principal debería ser evitar necesitarlo. El backup es una capa adicional de protección. 🧪 Probar rollback también es buena idea No basta con decir: “Si algo falla hacemos rollback.” Conviene saber: ¿Cómo? ¿Cuánto tarda? ¿Qué pasa con la base? ¿La versión anterior sigue siendo compatible? Si nunca lo has probado, quizá descubras los problemas cuando ya estás en medio de un incidente. 🚨 Observabilidad después de una migración Incluso después de ejecutar correctamente el cambio, conviene observar: Errores 500 Latencia Consultas lentas Deadlocks Replication lag Uso de CPU Locks Una migración puede no romper inmediatamente la aplicación. Pero puede degradar el rendimiento. Por ejemplo: Antes: query = 20 ms Después: query = 2 segundos Técnicamente todo funciona. Pero existe un problema. 📊 Métricas antes y después Una buena práctica consiste en comparar: ANTES DEL DEPLOY ↓ Errores Latencia CPU DB load con: DESPUÉS DEL DEPLOY ↓ Errores Latencia CPU DB load Si algo cambia bruscamente, puedes reaccionar rápido. 🚀 Un ejemplo de migración segura completa Supongamos que queremos reemplazar: users.username por: users.display_name Una estrategia podría ser: Deploy 1 ↓ Agregar display_name nullable Después: Proceso batch ↓ Copiar username → display_name Luego: Deploy 2 ↓ Leer display_name ↓ Mantener compatibilidad con username Después: Monitorear ↓ Confirmar migración Luego: Deploy 3 ↓ Dejar de utilizar username Y finalmente: Migración posterior ↓ Eliminar username Más pasos. Pero mucho menos riesgo. ⚠️ A veces "más lento" es más rápido Puede parecer exagerado hacer tres deploys para cambiar una columna. Pero comparémoslo. Estrategia rápida Renombrar columna ↓ Deploy ↓ Producción rota ↓ Investigar ↓ Rollback ↓ Incidente Estrategia gradual Agregar ↓ Migrar ↓ Validar ↓ Eliminar La segunda parece más larga. Pero puede ahorrar horas de incidentes. 🧠 No todas las migraciones necesitan tanta complejidad Naturalmente, no cada cambio requiere múltiples despliegues. Por ejemplo, agregar una tabla completamente nueva que ninguna versión anterior utiliza puede ser relativamente sencillo. El nivel de cuidado debería depender del riesgo. Pregúntate: ¿Es destructivo? ¿Afecta columnas existentes? ¿Hay muchas filas? ¿Existe tráfico mientras se ejecuta? ¿Varias versiones convivirán? ¿Puedo volver atrás? Mientras más respuestas sean “sí”, más cuidado necesitas. 🛠️ Buenas prácticas ✔️ Prefiere cambios compatibles hacia atrás. ✔️ Divide cambios destructivos en varias etapas. ✔️ Agrega estructuras nuevas antes de eliminar las anteriores. ✔️ Migra grandes cantidades de datos por lotes cuando sea necesario. ✔️ Comprueba cuánto tardará una migración con datos representativos. ✔️ Prueba en staging. ✔️ Considera servidores, workers y consumidores externos. ✔️ Diseña un rollback antes del deploy. ✔️ Evita eliminar datos hasta estar seguro de que ya no son necesarios. ✔️ Monitorea el comportamiento de la base después del cambio. 🧩 La realidad Actualizar una base de datos en producción no consiste simplemente en ejecutar: python manage.py migrate o: npm run migrate y esperar que todo salga bien. Una migración forma parte de un sistema vivo. Mientras cambia la base de datos puede haber: Usuarios enviando peticiones Servidores antiguos Servidores nuevos Workers procesando tareas APIs consultando información Todos trabajando sobre la misma estructura. La pregunta no debería ser únicamente: ¿La migración funciona? También: ¿La versión anterior sigue funcionando? ¿La nueva versión puede convivir con ella? ¿Podemos volver atrás? ¿El cambio bloqueará la base? 💬 Una buena migración no es solamente aquella que termina con: Migration completed successfully Es aquella que permite que código y base de datos evolucionen sin dejar de entenderse mientras ocurre el cambio. 👉 La próxima vez que quieras eliminar o renombrar una columna en producción, antes de ejecutar la migración pregúntate: ¿Existe todavía algún proceso utilizando esta estructura en este preciso momento? Esa pregunta puede evitar un incidente completo. 🔥 El backend no se ve, pero sin él, nada funciona.

30 ago 2026
Leer publicación
Tu código funciona perfectamente… pero eso no significa que esté listo para producción.
DevOps

Tu código funciona perfectamente… pero eso no significa que esté listo para producción.

Terminas una nueva funcionalidad. La pruebas en tu computadora. Todo funciona. No aparecen errores. La base de datos responde. Las peticiones funcionan. Entonces parece lógico pensar: 👉 “Vamos a subirla a producción.” Pero producción no es tu computadora. Producción es donde están: 👤 Usuarios reales. 💳 Operaciones reales. 🗄️ Datos importantes. 📦 Procesos que no puedes romper. 🌐 Integraciones externas. Y un pequeño error puede terminar provocando: ❌ Datos incorrectos. ❌ Servicios caídos. ❌ Compras fallidas. ❌ Funcionalidades rotas. ❌ Pérdida de confianza. Por eso muchos equipos utilizan un entorno intermedio antes de publicar cambios: Staging. 🧠 ¿Qué es un entorno de staging? Staging es un entorno diseñado para probar una aplicación antes de llevarla a producción. La idea es que se parezca lo máximo posible al entorno real. Por ejemplo, puede utilizar: La misma versión del sistema operativo Las mismas dependencias La misma configuración general Una base de datos separada Servicios similares Variables de entorno equivalentes Pero existe una diferencia fundamental: 👉 Los usuarios reales no dependen de staging. Eso permite probar cambios de forma mucho más segura. ⚙️ ¿Cómo suele organizarse un proyecto? Un flujo sencillo puede ser: Desarrollo ↓ Pruebas ↓ Staging ↓ Producción Cada entorno cumple una función diferente. Desarrollo Aquí escribes y modificas código. Puedes experimentar. Puedes romper cosas. El objetivo es construir. Pruebas Aquí pueden ejecutarse: Tests unitarios Tests de integración Tests automáticos para comprobar que ciertas partes del sistema continúan funcionando. Staging Aquí se despliega una versión muy cercana a la que posteriormente llegará a producción. El objetivo es probar el sistema completo en condiciones más realistas. Producción Finalmente: Usuarios reales Datos reales Operación real Aquí cualquier error tiene consecuencias reales. 🚀 ¿Por qué no basta con probar en local? Porque tu computadora probablemente no tiene exactamente el mismo entorno que producción. Por ejemplo, podrías trabajar con: Windows mientras producción utiliza: Linux O quizá localmente tienes: Node.js 22 pero producción utiliza otra versión. También pueden existir diferencias en: Variables de entorno. Certificados. Redes. Dominios. Base de datos. Permisos. Servicios externos. Servidores web. Contenedores. Entonces aparece el clásico: “En mi máquina sí funciona.” Y probablemente sea cierto. El problema es que tu máquina no es producción. 🧩 Un ejemplo real Imagina que estás construyendo una tienda en línea. Modificas el flujo de compra. En tu computadora haces: Agregar producto ↓ Pagar ↓ Crear pedido ↓ Mostrar confirmación Todo funciona. Entonces despliegas directamente a producción. Pero descubres que una variable de entorno del servicio de pagos no está configurada correctamente. Ahora: Usuarios ↓ Comprar ↓ Pago ↓ ❌ Error Si hubieras desplegado primero en staging, probablemente habrías detectado ese problema antes. 🗄️ Las migraciones son otro gran ejemplo Supongamos que agregas una nueva columna: usuarios.telefono En desarrollo ejecutas: migración ↓ Correcto Pero la base de datos de producción tiene: millones de registros La misma migración puede comportarse de manera muy diferente. Por ejemplo: Bloquear tabla Tardar demasiado Consumir muchos recursos Fallar por datos existentes Staging permite probar ese tipo de cambios en un entorno más parecido al real. ⚠️ Pero staging también debe tener datos representativos Supongamos que producción tiene: 5,000,000 registros y staging solamente: 20 registros Una consulta podría responder perfectamente en staging: 10 ms pero tardar en producción: 8 segundos Por eso, cuando sea posible, staging debería utilizar volúmenes de datos que permitan detectar este tipo de problemas. Eso no significa copiar directamente toda la base de producción. Los datos deben manejarse con cuidado y sin exponer información sensible. 🔐 No uses datos reales sin protección Un error peligroso sería copiar directamente: Nombres reales Correos reales Teléfonos reales Direcciones reales Información sensible hacia staging. ¿Por qué? Porque staging puede tener controles menos estrictos. Quizá más desarrolladores tengan acceso. O quizá las herramientas de monitoreo sean diferentes. Una estrategia más segura es utilizar: Datos ficticios Datos anonimizados Datos sanitizados que permitan simular producción sin exponer información real. 🌐 Las integraciones también pueden comportarse diferente Una aplicación puede depender de: Pagos Correos SMS APIs externas Almacenamiento Servicios de terceros En desarrollo quizá utilizas versiones de prueba. Por ejemplo: Stripe test mode o algún sandbox equivalente. Staging suele permitir probar esas integraciones de una manera más cercana al flujo real, pero sin afectar operaciones reales. 💳 Mucho cuidado con servicios externos Imagina que staging ejecuta accidentalmente: Cobro real en lugar de: Cobro de prueba Eso podría provocar problemas. Por eso los entornos deben tener claramente separadas sus credenciales. Por ejemplo: STAGING_PAYMENT_KEY y: PRODUCTION_PAYMENT_KEY Nunca deberían mezclarse accidentalmente. 🔑 Variables de entorno Las aplicaciones suelen utilizar variables como: DATABASE_URL API_KEY SECRET_KEY EMAIL_HOST STORAGE_BUCKET En desarrollo podrías tener: .env.development En staging: .env.staging Y producción: .env.production Cada entorno puede necesitar valores diferentes. El problema aparece cuando la estructura de configuración también cambia. ⚠️ Configuration Drift Imagina: Staging Nginx configuración A y: Producción Nginx configuración B O: Staging PostgreSQL 17 pero: Producción PostgreSQL 15 Cuantas más diferencias existan, menos confiables serán las pruebas. A esto se le puede relacionar con el concepto de configuration drift . Poco a poco los entornos se van separando. Y dejan de representar la misma aplicación. 🧠 El objetivo no es que sean idénticos en tamaño Staging no necesita necesariamente tener: 20 servidores solo porque producción los tenga. Eso podría ser demasiado costoso. Lo importante es reproducir las características que podrían afectar el comportamiento. Por ejemplo: Mismas versiones Misma arquitectura general Mismo tipo de base de datos Configuraciones equivalentes Mismos procesos de deploy Puede ser una versión más pequeña de producción. Pero debería comportarse de manera similar. 🐳 Los contenedores ayudan con esto Herramientas como Docker pueden ayudar a reducir diferencias. Por ejemplo: Imagen de aplicación ↓ Development Staging Production La misma imagen puede desplegarse en diferentes entornos. Solo cambian configuraciones específicas. Esto ayuda a evitar: “En staging usamos una versión diferente del código.” 🚀 El mismo artefacto debería avanzar entre entornos Una buena estrategia consiste en construir una versión: app:v2.4.0 y desplegar exactamente esa misma versión en: Staging ↓ Validación ↓ Producción En lugar de: Build para staging ↓ hacer cambios ↓ nuevo build diferente ↓ producción Porque si cambias el código después de probarlo, ya no estás desplegando exactamente lo que validaste. 🔄 CI/CD Aquí entra otro concepto importante: Continuous Integration / Continuous Delivery Un pipeline podría hacer: Push ↓ Tests ↓ Build ↓ Deploy staging ↓ Pruebas ↓ Deploy producción Esto reduce pasos manuales. Y, sobre todo, ayuda a que los despliegues sean repetibles. 🧪 ¿Qué deberías probar en staging? No solamente: “La página abre.” Conviene probar flujos importantes. Por ejemplo: Autenticación Login Logout Recuperar contraseña Ecommerce Crear carrito Comprar Cancelar pedido Administración Crear usuario Editar usuario Eliminar usuario Integraciones Enviar correo Procesar pago de prueba Subir archivo El objetivo es comprobar los procesos que realmente utilizan los usuarios. 🚨 Smoke Tests Después de desplegar una nueva versión en staging, puede ejecutarse un conjunto pequeño de pruebas críticas. Por ejemplo: ¿La aplicación responde? ¿El login funciona? ¿La base de datos conecta? ¿Los endpoints principales responden? A esto se le suele llamar smoke testing . La idea es detectar rápidamente si algo fundamental se rompió. 🔄 Regression Testing También es importante comprobar que una nueva funcionalidad no haya roto algo que antes funcionaba. Por ejemplo: Agregaste: Cupones pero accidentalmente rompiste: Pagos Las pruebas de regresión buscan detectar precisamente eso. Un cambio puede funcionar correctamente por sí mismo y aun así afectar otras partes. 🚀 Un ejemplo con una nueva versión Supongamos: Versión actual: v2.3 Construyes: v2.4 Primero: v2.4 ↓ Staging El equipo prueba: Login ✅ Pedidos ✅ Pagos ✅ Emails ✅ Migraciones ✅ Entonces: v2.4 ↓ Producción Esto reduce considerablemente el riesgo comparado con desplegar directamente. ⚠️ Staging no detecta todos los problemas Este punto es importante. Puedes probar perfectamente en staging y aun así tener errores en producción. ¿Por qué? Porque producción tiene condiciones difíciles de reproducir completamente. Por ejemplo: Mucho más tráfico Muchos usuarios concurrentes Datos históricos Problemas de red reales Patrones de uso inesperados Staging reduce el riesgo. No lo elimina. 📈 Un problema que puede aparecer solo con carga Supongamos que staging recibe: 5 requests/s pero producción recibe: 5,000 requests/s Una condición de carrera puede no aparecer nunca en staging. O una base de datos puede funcionar bien con poca carga y saturarse en producción. Para eso también existen pruebas específicas de carga. 🏋️ Load Testing Las pruebas de carga intentan simular muchos usuarios o solicitudes. Por ejemplo: 100 usuarios ↓ 500 usuarios ↓ 1,000 usuarios y observar: Latencia CPU Memoria Errores Base de datos Staging puede ser un buen entorno para este tipo de pruebas cuando está preparado para ello. 💥 Stress Testing El stress testing va un poco más lejos. En lugar de preguntar: “¿Funciona con nuestra carga normal?” pregunta: “¿En qué punto deja de funcionar?” Por ejemplo: 1,000 requests/s ✅ 2,000 requests/s ✅ 5,000 requests/s ⚠️ 8,000 requests/s ❌ Esto ayuda a conocer los límites del sistema. 🛡️ También puedes probar recuperación Staging puede utilizarse para probar: ¿Qué ocurre si reinicio el backend? ¿Qué ocurre si la base de datos falla? ¿Qué pasa si Redis no responde? ¿Qué ocurre si una API externa devuelve 503? Estas pruebas ayudan a validar mecanismos de resiliencia. 🗄️ Probar backups también es importante Tener: backup.sql no significa que realmente puedas recuperar la aplicación. Una práctica saludable es probar: Backup ↓ Restaurar en entorno controlado ↓ Verificar datos Staging o ambientes temporales pueden utilizarse para validar procesos de recuperación. Porque el peor momento para descubrir que tu backup no funciona es después de una pérdida real. ⚠️ Staging no debería enviar correos reales por accidente Imagina que pruebas una funcionalidad y la aplicación tiene: 50,000 usuarios en una base copiada o anonimizada parcialmente. Si staging está conectado al proveedor real de correo, podrías terminar enviando mensajes accidentalmente. Por eso suele ser útil configurar: Proveedor de prueba Inbox de testing Dominio controlado o bloquear envíos reales desde staging. 🧩 Lo mismo con notificaciones También conviene evitar: Push notifications reales SMS reales Webhooks reales cuando no correspondan. Un entorno de staging debe parecerse a producción técnicamente. Pero no debería provocar efectos reales peligrosos. 🔐 Acceso restringido Staging tampoco necesariamente debería estar abierto para todo Internet. Podría utilizar: VPN Autenticación IP allowlist Login adicional dependiendo del proyecto. Aunque no contenga información real, puede revelar: Funcionalidades nuevas Endpoints Errores Versiones antes de que lleguen a producción. 🕵️ Evita indexar staging Otro error común es tener: staging.midominio.com y permitir que buscadores lo indexen. Entonces Google podría mostrar versiones de prueba de tu sitio. Eso puede provocar: Contenido duplicado. Páginas incompletas. Información que aún no debía ser pública. Los entornos de staging deberían configurarse para evitar este tipo de exposición. 🚀 Staging también sirve para revisión del equipo Antes de publicar una funcionalidad, diferentes personas pueden revisarla. Por ejemplo: Desarrollador ↓ QA ↓ Diseño ↓ Producto ↓ Cliente interno Todos pueden acceder a la misma versión y comprobar: Diseño Flujo Texto Comportamiento Errores sin utilizar producción. 👀 QA Un equipo de Quality Assurance puede utilizar staging para validar escenarios que quizá el desarrollador no consideró. Por ejemplo: ¿Qué pasa si dejo el campo vacío? ¿Qué ocurre si hago doble clic? ¿Qué pasa desde celular? ¿Qué ocurre si pierdo conexión? El desarrollador tiende a probar: Cómo debería funcionar. QA también intenta comprobar: Cómo podría romperse. ⚠️ “Funciona” no significa “está listo” Imagina una funcionalidad que: Crea usuarios ✅ Pero: No valida permisos ❌ No tiene logs ❌ No maneja errores ❌ Rompe en móvil ❌ Técnicamente funciona. Pero quizá todavía no está preparada para producción. Staging da una oportunidad para evaluar el sistema completo, no solamente el caso exitoso. 📊 Monitorear staging también puede ser útil Puedes tener herramientas de: Logs Métricas Tracing APM también en staging. Así puedes observar cómo se comporta una nueva versión. Por ejemplo: Endpoint /checkout ↓ 2.5 segundos puede llamar la atención antes del deploy. 🐞 Los logs pueden revelar errores silenciosos Quizá visualmente: Todo parece funcionar. Pero los logs muestran: 500 Timeout Warning Query lenta Revisar observabilidad durante las pruebas puede detectar problemas que la interfaz no muestra claramente. 🚀 Feature Flags Otra estrategia interesante son los feature flags . Permiten desplegar código sin habilitar inmediatamente una funcionalidad para todos. Por ejemplo: Nueva búsqueda ↓ feature_search_v2 = false El código ya está desplegado. Pero permanece desactivado. Después: feature_search_v2 = true para ciertos usuarios. Esto puede complementar staging, aunque no lo reemplaza. 🧩 Staging vs Preview Environments En equipos modernos también pueden existir entornos temporales por cada rama o Pull Request. Por ejemplo: PR #452 ↓ preview-452.midominio.com Esto permite revisar un cambio específico sin modificar el staging compartido. Después los cambios aprobados pueden llegar a staging y posteriormente a producción. ⚠️ Demasiados entornos también tienen costo Cada ambiente adicional implica: Infraestructura Configuración Secretos Bases de datos Monitoreo Mantenimiento No todos los proyectos necesitan una arquitectura empresarial enorme. Una aplicación pequeña puede utilizar un flujo mucho más sencillo. Lo importante es que el proceso sea proporcional al riesgo. 🧠 ¿Un proyecto pequeño necesita staging? No necesariamente con la misma complejidad de una plataforma enorme. Pero si la aplicación ya tiene: Usuarios reales Pagos Datos importantes Clientes tener algún entorno seguro para probar antes de producción suele ser una inversión muy útil. Especialmente si varios desarrolladores participan. 🔄 Staging debería actualizarse regularmente Si staging tiene una versión de hace: 3 meses ya no representa producción. Lo ideal es que el flujo de despliegue lo mantenga relativamente actualizado. Por ejemplo: Nueva versión candidata ↓ Staging automáticamente Así el entorno sigue siendo útil. 🚨 Cambios manuales son peligrosos Supongamos que alguien entra directamente a producción y modifica: nginx.conf pero no aplica ese mismo cambio en staging. Ahora ambos entornos son diferentes. Semanas después: Staging ✅ Producción ❌ porque nadie recordó esa modificación manual. Por eso la infraestructura y configuración deberían automatizarse tanto como sea razonable. 🏗️ Infrastructure as Code Herramientas de infraestructura como código permiten definir: Servidores Redes Servicios Configuraciones de forma reproducible. Esto reduce diferencias entre entornos. En lugar de: “Configura staging más o menos como producción.” puedes tener configuraciones declaradas y versionadas. 🔁 El deploy también debería ser similar Imagina que staging se despliega con: Docker + CI/CD pero producción con: Copiar archivos manualmente por FTP No estás probando realmente el mismo proceso. Idealmente: Mismo pipeline Mismo artefacto Mismo proceso con diferentes credenciales y configuraciones. Esto reduce sorpresas. 🚀 Ejemplo de pipeline Podría verse así: git push ↓ Tests automáticos ↓ Build ↓ Crear imagen Docker ↓ Deploy staging ↓ Smoke tests ↓ Aprobación ↓ Deploy producción El objetivo es que el camino hacia producción sea predecible. 🧯 ¿Y si producción falla de todos modos? Incluso después de staging puede ocurrir. Por eso también necesitas una estrategia de recuperación. Por ejemplo: v2.4 ↓ Producción ↓ Problema ↓ Rollback ↓ v2.3 Staging reduce la posibilidad de necesitarlo. Pero no reemplaza tener un plan de rollback. 🔄 Rollback Un rollback permite regresar a una versión anterior cuando la nueva tiene un problema crítico. Pero hay que tener cuidado con las migraciones. Por ejemplo: Código v2 ↓ Migración cambia estructura BD Volver simplemente a: Código v1 puede no funcionar si la base de datos ya cambió. Por eso el diseño de migraciones también forma parte de una estrategia de despliegue segura. 🧠 Staging es una capa, no una garantía Podemos pensar: Tests ↓ Staging ↓ Producción ↓ Monitoreo Cada capa reduce un tipo de riesgo. Pero ninguna es perfecta. No debes pensar: “Pasó staging, así que producción nunca fallará.” La mentalidad correcta es: “Pasó una capa más de validación antes de afectar usuarios reales.” 🛠️ Buenas prácticas ✔️ Mantén staging lo más parecido posible a producción. ✔️ Utiliza las mismas versiones de dependencias y servicios importantes. ✔️ Mantén bases de datos separadas. ✔️ Nunca utilices credenciales reales accidentalmente. ✔️ Utiliza datos ficticios, anonimizados o sanitizados. ✔️ Prueba migraciones antes de producción. ✔️ Utiliza el mismo proceso de despliegue entre entornos. ✔️ Automatiza pruebas importantes. ✔️ Evita cambios manuales difíciles de reproducir. ✔️ Mantén staging protegido y fuera de buscadores. 🧩 La realidad Cuando una funcionalidad funciona en tu computadora, solamente has demostrado una cosa: Funciona en tu computadora. Todavía falta comprobar: ¿Funciona con la configuración real? ¿Funciona con la base correcta? ¿Funcionan las migraciones? ¿Funcionan las integraciones? ¿Funciona después del deploy? ¿Sigue funcionando lo que ya existía? Staging intenta responder esas preguntas antes de que los usuarios reales tengan que hacerlo por ti. 💬 El objetivo de staging no es eliminar todos los errores. Es darte una última oportunidad de encontrar tantos como sea posible antes de que lleguen a producción. Porque existe una diferencia enorme entre: “Funcionó en mi computadora.” y: “Está preparado para que usuarios reales dependan de él.” 👉 La próxima vez que una funcionalidad esté lista para hacer deploy, no preguntes solamente: “¿Funciona?” Pregunta también: “¿Ya la probamos en un entorno que realmente se parezca a producción?” 🔥 El backend no se ve, pero sin él, nada funciona.

29 ago 2026
Leer publicación
Entras a una página web… pero el servidor no te envía todo listo
Frontend

Entras a una página web… pero el servidor no te envía todo listo

Abres una aplicación web. Durante un instante aparece una estructura básica. Después carga JavaScript. La aplicación consulta información. Y poco a poco comienza a construir lo que ves en pantalla. Menús. Tarjetas. Tablas. Gráficas. Botones. Datos del usuario. 👉 Ese enfoque se conoce como Client-Side Rendering , o simplemente CSR . 🧠 ¿Qué es el Client-Side Rendering? El Client-Side Rendering significa que una parte importante de la interfaz se genera directamente en el navegador del usuario. El servidor no necesariamente envía toda la página completamente construida. En cambio, puede entregar algo como: HTML básico + CSS + JavaScript Después el navegador ejecuta ese JavaScript. Y es el propio frontend quien comienza a construir la interfaz. Podemos imaginarlo así: Servidor ↓ HTML básico CSS JavaScript ↓ Navegador ↓ Ejecutar JavaScript ↓ Construir interfaz Esto es muy común en aplicaciones modernas desarrolladas con herramientas como React, Vue o Angular. 📄 ¿Qué HTML recibe inicialmente el navegador? En una aplicación basada principalmente en CSR, el HTML inicial puede ser bastante pequeño. Por ejemplo: <body> <div id="root"></div> <script src="app.js"></script> </body> A simple vista, prácticamente no existe contenido. Solo tenemos: <div id="root"> Después JavaScript utiliza ese elemento como punto de entrada para construir la aplicación. Por ejemplo: root ├── Navbar ├── Sidebar ├── Dashboard ├── Botones └── Formularios Todo eso puede aparecer después de que el navegador ejecute el código. ⚙️ ¿Cómo funciona el flujo completo? De forma simplificada: 1. Navegador solicita página ↓ 2. Servidor devuelve HTML ↓ 3. Navegador descarga CSS y JavaScript ↓ 4. Ejecuta JavaScript ↓ 5. La aplicación inicia ↓ 6. Consulta APIs si necesita datos ↓ 7. Construye la interfaz Eso significa que JavaScript tiene un papel mucho más importante que en una página HTML tradicional. No se utiliza únicamente para añadir pequeñas interacciones. Puede encargarse de administrar gran parte de la aplicación. 🚀 Un ejemplo con React Imagina un componente: function Perfil() { return ( <div> <h1>Mi perfil</h1> <button>Editar</button> </div> ); } React toma esa descripción de la interfaz y termina reflejándola en el DOM del navegador. Conceptualmente: Componente React ↓ JavaScript ↓ DOM ↓ Pantalla El servidor no tuvo que enviar directamente ese HTML completo como resultado final. Fue construido desde el navegador. 📡 ¿Qué ocurre con los datos? Normalmente una aplicación necesita información dinámica. Por ejemplo: Nombre del usuario Pedidos Productos Estadísticas Notificaciones El JavaScript del frontend puede consultar una API. Por ejemplo: GET /api/usuarios/25 El backend responde: { "id": 25, "nombre": "Herman", "email": "correo@email.com" } Después el frontend utiliza esos datos para construir: Herman correo@email.com en la interfaz. 🧩 Entonces el backend sigue siendo necesario Este punto es importante. CSR no significa: Frontend reemplaza al backend Significa que la responsabilidad de renderizar gran parte de la interfaz se mueve al navegador. El backend puede seguir encargándose de: Autenticación. Usuarios. Base de datos. Pagos. Permisos. Inventario. Reglas de negocio. APIs. Podemos pensar: Backend ↓ Entrega datos Frontend ↓ Decide cómo mostrarlos 🚀 Un ejemplo real: un dashboard Imagina un panel administrativo. Después de iniciar sesión ves: 📊 Estadísticas 👤 Usuarios 📦 Productos 🛒 Pedidos ⚙️ Configuración Cuando seleccionas: Usuarios la aplicación no necesariamente solicita una página HTML completamente nueva. Puede hacer: Click ↓ JavaScript cambia sección ↓ GET /api/usuarios ↓ Backend responde JSON ↓ Frontend renderiza usuarios Después seleccionas: Productos y ocurre: GET /api/productos La estructura general de la aplicación continúa cargada. Solo cambia la parte necesaria. ⚡ Aquí aparece el concepto de SPA Muchas aplicaciones con CSR también funcionan como Single Page Applications , o SPA. Una SPA carga una estructura principal y después cambia la interfaz utilizando JavaScript. Por ejemplo: /dashboard ↓ /usuarios ↓ /productos ↓ /pedidos Desde el punto de vista del usuario parecen páginas diferentes. Pero técnicamente puede seguir siendo la misma aplicación JavaScript modificando lo que se muestra. 🌐 ¿Entonces la URL no cambia? Sí puede cambiar. Frameworks frontend suelen utilizar un router. Por ejemplo: /dashboard puede mostrar: DashboardComponent Mientras: /usuarios muestra: UsersComponent El router del frontend detecta la URL y decide qué componente renderizar. No siempre es necesario hacer una recarga completa del documento. 🛣️ Navegación tradicional vs CSR En una página tradicional podríamos tener: Usuario hace clic ↓ Navegador solicita nueva página ↓ Servidor genera HTML ↓ Navegador reemplaza documento Con CSR: Usuario hace clic ↓ JavaScript detecta navegación ↓ Cambia componentes ↓ Solicita datos necesarios ↓ Actualiza DOM La segunda estrategia puede hacer que navegar dentro de la aplicación se sienta muy rápido. 🚀 Una gran ventaja: interactividad CSR funciona especialmente bien cuando tienes muchas interacciones. Por ejemplo: Filtros Tablas Modales Drag & Drop Gráficas Formularios dinámicos Actualizaciones en tiempo real El navegador ya tiene cargada gran parte de la lógica. Puede responder inmediatamente a muchas acciones. Por ejemplo: Usuario abre modal ↓ JavaScript ↓ Mostrar modal No necesitas preguntar al servidor simplemente para mostrar una ventana. 🔄 La interfaz puede cambiar sin recargar Imagina un carrito: Carrito: 2 Presionas: Agregar producto El frontend puede cambiar: Carrito: 3 al instante. Y después sincronizar esa operación con el backend. La experiencia se siente más parecida a una aplicación de escritorio que a una página web tradicional. 🧠 El estado se vuelve muy importante En CSR, el frontend puede tener que recordar información mientras el usuario navega. Por ejemplo: Usuario autenticado Productos seleccionados Filtros Carrito Tema oscuro Formulario parcialmente completado A esto normalmente se le llama estado . Podemos imaginar: Estado ↓ Interfaz Cuando el estado cambia: carrito = 2 ↓ carrito = 3 la interfaz puede actualizarse. Frameworks modernos ayudan precisamente a administrar estas relaciones. 🚀 Ejemplo de estado Imagina: likes = 20 La interfaz muestra: ❤️ 20 El usuario presiona: Me gusta Ahora: likes = 21 Y React puede actualizar solamente: ❤️ 21 sin reconstruir manualmente toda la página. 📦 Pero primero hay que descargar JavaScript Aquí aparece uno de los principales costos del CSR. Antes de mostrar gran parte de la interfaz, el navegador puede necesitar descargar bastante JavaScript. Por ejemplo: app.js vendor.js components.js Después tiene que: Descargar ↓ Descomprimir ↓ Parsear ↓ Compilar ↓ Ejecutar Y solo después puede comenzar a construir algunas partes de la aplicación. Eso puede afectar la primera carga. 📱 El dispositivo del usuario también importa Supongamos que tienes: 300 KB de JavaScript En una computadora potente puede ejecutarse rápidamente. Pero en un teléfono de gama baja: CPU más lenta Menos memoria el mismo JavaScript puede tardar considerablemente más. Entonces CSR depende no solamente del servidor. También depende de la capacidad del dispositivo del usuario. 🌐 Una conexión lenta también afecta El usuario necesita descargar los bundles. Con conexión rápida: JavaScript ↓ rápido Pero con una conexión móvil lenta: JavaScript ↓ esperar ↓ ejecutar ↓ mostrar contenido La primera carga puede sentirse lenta. Por eso es importante controlar cuánto JavaScript enviamos. ✂️ Code Splitting Una estrategia para evitar enviar todo desde el inicio es dividir el JavaScript. Esto se conoce como: Code Splitting En lugar de: app.js ↓ 2 MB podrías tener: main.js dashboard.js users.js reports.js settings.js Y descargar cada parte cuando sea necesaria. 🚀 Ejemplo El usuario entra a: /dashboard Entonces necesita: main.js dashboard.js Pero todavía no necesita: reports.js Cuando posteriormente entra a reportes: /reports se descarga esa sección. Esto reduce el trabajo inicial. 💤 Lazy Loading Esta idea también aparece con componentes. Por ejemplo: Dashboard ↓ Cargar ahora Panel avanzado ↓ Cargar cuando se abra A esto se le conoce como lazy loading . La idea es no descargar o procesar algo antes de necesitarlo. ⚠️ El problema del contenido inicial En CSR puro, puede ocurrir algo así: HTML llega ↓ Pantalla casi vacía ↓ JavaScript carga ↓ JavaScript ejecuta ↓ API responde ↓ Contenido aparece Eso puede generar una sensación de espera. Por eso muchas aplicaciones utilizan: Skeleton loaders Spinners Placeholders mientras se obtienen los datos. 💀 El famoso loading Por ejemplo: Cargando... puede aparecer porque el frontend todavía está esperando: GET /api/dashboard El servidor inicial ya entregó la aplicación. Pero los datos necesarios para completar la pantalla todavía no llegaron. 🚀 La navegación posterior suele ser rápida Aunque la primera carga pueda ser más pesada, las navegaciones posteriores pueden sentirse muy fluidas. Porque ya tienes: Framework Router Componentes principales Estilos Lógica en el navegador. Entonces cambiar entre secciones puede requerir solamente: Consulta pequeña + Renderizado parcial en lugar de cargar todo desde cero. 🔍 ¿Qué ocurre con SEO? Aquí aparece otra consideración importante. Imagina que un buscador solicita: /productos/laptop Y el servidor responde inicialmente con: <div id="root"></div> El contenido real aparece solo después de ejecutar JavaScript. Los buscadores modernos pueden procesar JavaScript en ciertos contextos, pero depender completamente de ello puede hacer más complejo el SEO y la indexación. Por eso sitios donde el contenido público es muy importante pueden utilizar otras estrategias. 📄 Ejemplo de contenido público Piensa en: Blog Noticias Tienda pública Página de producto Landing page Aquí suele ser conveniente que el contenido importante llegue rápidamente dentro del HTML. Esto puede ayudar con: SEO. Primera carga. Compartir enlaces. Previsualizaciones. CSR puro puede no ser siempre la mejor opción. 🧩 Pero en un dashboard el SEO casi no importa Ahora imagina: admin.empresa.com Solo usuarios autenticados pueden entrar. Google no necesita indexar: Dashboard de ventas Lista de empleados Configuración En ese escenario, CSR puede ser excelente. La prioridad está en la interacción. No en posicionar la página en buscadores. 🏢 Por eso CSR funciona muy bien en aplicaciones internas Casos comunes: ✔️ CRM. ✔️ ERP. ✔️ Dashboards. ✔️ Paneles administrativos. ✔️ Sistemas internos. ✔️ Plataformas empresariales. ✔️ Herramientas altamente interactivas. Después de iniciar sesión, el usuario puede permanecer horas dentro de la aplicación. La inversión inicial de cargar JavaScript puede tener mucho sentido. ⚠️ CSR no significa mejor automáticamente Es fácil pensar: React = moderno CSR = mejor pero no necesariamente. Si haces una página muy sencilla: Título Texto Imagen Formulario de contacto quizá no necesites descargar una enorme aplicación JavaScript para mostrarla. La arquitectura debe adaptarse al problema. 🧠 SSR como alternativa Otra estrategia es: Server-Side Rendering o SSR. Aquí el servidor genera el HTML antes de enviarlo. Conceptualmente: Request ↓ Servidor obtiene datos ↓ Servidor genera HTML ↓ Navegador recibe contenido listo Por ejemplo, puede enviar directamente: <h1>Producto</h1> <p>$500</p> en lugar de esperar a que JavaScript construya ese contenido. ⚖️ CSR vs SSR Simplificando: CSR Servidor ↓ JavaScript ↓ Navegador genera interfaz SSR Servidor genera interfaz ↓ HTML ↓ Navegador Cada enfoque tiene ventajas y costos diferentes. 🚀 También existen estrategias híbridas Actualmente muchas herramientas no obligan a elegir solamente una opción. Frameworks modernos permiten combinar: SSR CSR SSG ISR dependiendo de cada página. Por ejemplo: Landing → SSR/SSG Blog → SSG Dashboard → CSR dentro del mismo proyecto. Esto permite elegir la estrategia adecuada para cada sección. 🧩 ¿Qué es SSG? Static Site Generation significa generar las páginas previamente. Por ejemplo, durante un build: Artículo 1 → HTML Artículo 2 → HTML Artículo 3 → HTML Después esos archivos pueden entregarse rápidamente. Es especialmente útil para contenido que no cambia constantemente. 🔄 ¿Y qué ocurre con la hidratación? En aplicaciones con renderizado híbrido puede aparecer un concepto llamado hydration . El servidor puede enviar HTML ya construido. Por ejemplo: Botón visible Contenido visible Después JavaScript se carga y conecta la lógica interactiva. Conceptualmente: HTML generado en servidor ↓ Usuario ya ve contenido ↓ JavaScript carga ↓ Agregar interactividad Así se intenta combinar una carga inicial rápida con una interfaz dinámica. ⚠️ En CSR puro no necesitas hidratar HTML generado por el servidor Si el contenido completo fue creado directamente por JavaScript en el navegador: HTML básico ↓ JavaScript construye aplicación estamos principalmente ante renderizado del lado del cliente. La hidratación suele aparecer cuando servidor y cliente colaboran en el renderizado inicial. 🔐 Autenticación en aplicaciones CSR Un dashboard puede necesitar comprobar: ¿Existe sesión? La aplicación carga. Después consulta al backend. Por ejemplo: GET /api/me El servidor responde: { "id": 25, "nombre": "Herman", "rol": "admin" } Entonces el frontend decide qué mostrar. Pero la seguridad real no debería depender únicamente de ocultar componentes. 🚨 Ocultar una pantalla no protege una API Supongamos que un usuario normal no ve: Panel de administradores Eso está bien. Pero si intenta llamar: DELETE /api/usuarios/25 el backend debe comprobar sus permisos. CSR controla la interfaz. El backend sigue controlando autorización y reglas de negocio. 📡 El frontend puede consumir múltiples APIs Una sola pantalla puede hacer: GET /api/me GET /api/stats GET /api/orders GET /api/notifications Y combinar todo: Perfil + Estadísticas + Pedidos + Notificaciones en una misma interfaz. Esto da mucha flexibilidad. Pero también significa que debes cuidar la cantidad de peticiones. ⚠️ Demasiadas solicitudes iniciales Imagina que una página necesita: Request 1 Request 2 Request 3 ... Request 20 antes de estar lista. Aunque cada API sea rápida, la suma puede retrasar la experiencia. Por eso también conviene pensar en: Paralelización. Caché. APIs agregadas. Precarga. Evitar datos innecesarios. CSR no elimina los problemas de rendimiento. Simplemente los mueve a otras partes. 🧠 Caché del lado del cliente Una aplicación puede conservar temporalmente ciertos datos. Por ejemplo: GET /api/categorias ↓ Guardar temporalmente Después el usuario vuelve a la pantalla: Categorías ya disponibles y quizá no sea necesario consultar nuevamente de inmediato. Herramientas frontend modernas suelen ayudar con este tipo de gestión. 🔄 Pero también aparece la invalidación Supongamos que guardaste: Productos en caché. Después alguien crea uno nuevo. Ahora tienes que decidir: ¿Actualizar caché? ¿Volver a consultar? ¿Esperar a que expire? El problema de datos desactualizados también existe del lado del cliente. 🚀 CSR puede sentirse muy parecido a una aplicación nativa Una vez cargada, una SPA bien construida puede ofrecer: Navegación instantánea Transiciones Modales Actualizaciones parciales Estado persistente sin recargar el documento completo. Por eso herramientas empresariales modernas adoptan mucho este enfoque. El navegador deja de ser simplemente un lector de documentos. Se convierte en un verdadero entorno de ejecución. ⚠️ JavaScript se vuelve una dependencia crítica En una página tradicional, aunque JavaScript falle, quizá todavía puedas leer gran parte del HTML. En CSR puro: JavaScript falla ↓ Aplicación puede no aparecer Porque JavaScript es quien construye la interfaz. Esto hace que: Errores JS Bundles dañados Problemas de compatibilidad tengan un impacto mucho mayor. 🧪 Por eso el frontend también necesita observabilidad Conviene observar errores como: Unhandled exception Failed API request Bundle loading error Render error Herramientas de monitoreo frontend pueden ayudar a detectar cuándo usuarios reales están teniendo problemas. No basta con monitorear solamente el backend. 📊 Rendimiento en CSR Algunas métricas importantes pueden estar relacionadas con: Cuánto tarda en aparecer contenido Cuándo puede interactuar el usuario Cuánto JavaScript se descarga Cuánto tarda en ejecutarse Una aplicación puede descargar rápido, pero tardar demasiado ejecutando código. Entonces el usuario sigue sintiendo lentitud. 🧩 Menos JavaScript suele ayudar Una tentación común es agregar dependencias constantemente. Librería para fecha Librería para modal Librería para animación Librería para tabla Librería para todo Poco a poco el bundle crece. Por eso conviene revisar: ¿Qué dependencias necesito realmente? Reducir JavaScript puede mejorar considerablemente la primera carga. 🚀 Preloading y Prefetching En algunos casos puedes anticipar qué necesitará el usuario. Por ejemplo: Está en: /dashboard y probablemente después visite: /orders La aplicación puede comenzar a descargar ciertos recursos anticipadamente. Así cuando haga clic: /orders parte del trabajo ya está hecho. ⚠️ Pero tampoco conviene precargar todo Si precargas: Todos los módulos Todas las imágenes Todos los datos terminas anulando el beneficio del lazy loading. Otra vez aparece el equilibrio: Cargar lo suficiente sin cargar demasiado 🛠️ ¿Cuándo suele tener sentido CSR? Puede ser muy buena opción cuando: ✔️ La aplicación es altamente interactiva. ✔️ El SEO no es la prioridad principal. ✔️ El usuario permanece mucho tiempo dentro de la aplicación. ✔️ Existen muchas navegaciones internas. ✔️ El contenido depende de la sesión. ✔️ Tenemos dashboards y herramientas administrativas. ✔️ Queremos una experiencia similar a una app. ⚠️ ¿Cuándo deberías evaluar otras estrategias? Puede ser conveniente considerar SSR, SSG o enfoques híbridos cuando: ✔️ El contenido público debe posicionarse en buscadores. ✔️ La primera carga es especialmente importante. ✔️ El usuario necesita leer contenido inmediatamente. ✔️ Los dispositivos objetivo tienen poca capacidad. ✔️ Existe poco comportamiento interactivo. La decisión debe depender del producto. 🧠 Un ejemplo mixto Imagina una tienda en línea. Podrías tener: Home ↓ SSR/SSG Producto ↓ SSR/SSG Panel del usuario ↓ CSR Administración ↓ CSR No tienes que utilizar exactamente la misma estrategia en todo el sistema. ⚠️ Error común: pensar que renderizado y arquitectura backend son lo mismo Puedes tener: Frontend CSR con: Backend monolítico O: Frontend CSR con: Microservicios O incluso: SSR con cualquiera de los anteriores. Son decisiones diferentes. Una habla de: Dónde se genera la interfaz La otra: Cómo está organizado el backend 🛠️ Buenas prácticas ✔️ Reduce el JavaScript inicial cuando sea posible. ✔️ Utiliza code splitting para secciones grandes. ✔️ Implementa lazy loading donde tenga sentido. ✔️ Diseña estados de carga claros. ✔️ Maneja correctamente errores de API. ✔️ Utiliza caché con cuidado. ✔️ Protege las operaciones en el backend, no únicamente en la interfaz. ✔️ Evalúa el impacto en SEO antes de elegir CSR puro. ✔️ Prueba en teléfonos y conexiones más lentas. ✔️ Elige la estrategia según la aplicación, no únicamente por tendencia. 🧩 La realidad Con Client-Side Rendering, el navegador deja de ser solamente un lugar donde mostrar un documento enviado por el servidor. Se convierte en una parte importante de la aplicación. El flujo puede ser: Servidor ↓ HTML básico CSS JavaScript ↓ Navegador ↓ Ejecutar aplicación ↓ Consultar APIs ↓ Administrar estado ↓ Construir interfaz ↓ Actualizarla conforme interactúa el usuario Esto permite construir interfaces extremadamente dinámicas. Pero también significa que parte del trabajo que antes ocurría en el servidor ahora depende de: La conexión El dispositivo El tamaño del JavaScript La velocidad de ejecución Por eso CSR no es simplemente una tecnología de frontend. También es una decisión arquitectónica. 💬 No existe una única forma correcta de renderizar una aplicación. CSR puede ser excelente para un dashboard y una mala elección para una página donde el contenido debe aparecer inmediatamente. La clave está en decidir dónde conviene hacer el trabajo . 👉 La próxima vez que navegues por una aplicación sin ver una sola recarga completa, probablemente JavaScript esté haciendo mucho más trabajo en tu navegador de lo que parece. 🔥 El backend no se ve, pero sin él, nada funciona.

28 ago 2026
Leer publicación