<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ARTURO SEBASTIAN CUTIPA FLORES</title>
    <description>The latest articles on DEV Community by ARTURO SEBASTIAN CUTIPA FLORES (@arturo_sebastiancutipaf).</description>
    <link>https://dev.to/arturo_sebastiancutipaf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160458%2Fa8ee3ef8-b04d-4195-8633-4d483a1b3378.png</url>
      <title>DEV Community: ARTURO SEBASTIAN CUTIPA FLORES</title>
      <link>https://dev.to/arturo_sebastiancutipaf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/arturo_sebastiancutipaf"/>
    <language>en</language>
    <item>
      <title>Borramos la tabla de producción a propósito: una prueba de desastre real con PostgreSQL, Neon y Render</title>
      <dc:creator>ARTURO SEBASTIAN CUTIPA FLORES</dc:creator>
      <pubDate>Sun, 04 Oct 2026 04:48:02 +0000</pubDate>
      <link>https://dev.to/arturo_sebastiancutipaf/borramos-la-tabla-de-produccion-a-proposito-una-prueba-de-desastre-real-con-postgresql-neon-y-52na</link>
      <guid>https://dev.to/arturo_sebastiancutipaf/borramos-la-tabla-de-produccion-a-proposito-una-prueba-de-desastre-real-con-postgresql-neon-y-52na</guid>
      <description>&lt;p&gt;&lt;strong&gt;Autor:&lt;/strong&gt; Arturo Cutipa Flores&lt;br&gt;
&lt;strong&gt;Curso:&lt;/strong&gt; Base de Datos II · Universidad Privada de Tacna&lt;br&gt;
&lt;strong&gt;Proyecto grupal:&lt;/strong&gt; DB Backup Strategies, desarrollado junto con Carlos Jimenez Laura&lt;br&gt;
Hablar de backups es fácil; demostrar que funcionan es otra cosa. En nuestro proyecto decidimos no conformarnos con mostrar un archivo de respaldo: eliminamos la tabla principal de la base de datos de producción y medimos cuánto tardábamos en recuperarla. En este artículo cuento cómo planificamos esa prueba, qué salió mal en el primer intento y qué métricas obtuvimos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enlaces del proyecto&lt;/strong&gt;&lt;br&gt;
• Repositorio: &lt;a href="https://github.com/ArturoCutipaFlores/db-backup-strategies" rel="noopener noreferrer"&gt;https://github.com/ArturoCutipaFlores/db-backup-strategies&lt;/a&gt;&lt;br&gt;
• Aplicación en producción: &lt;a href="https://db-backup-strategies.onrender.com" rel="noopener noreferrer"&gt;https://db-backup-strategies.onrender.com&lt;/a&gt;&lt;br&gt;
• Video de demostración: &lt;a href="https://youtu.be/IchI-xmC1Ic" rel="noopener noreferrer"&gt;https://youtu.be/IchI-xmC1Ic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El escenario&lt;/strong&gt;&lt;br&gt;
La aplicación es un gestor de tareas (crear, completar, eliminar) con un frontend en HTML y JavaScript que consume una API en Node.js. Los datos se guardan en PostgreSQL 17 en Neon y la aplicación corre en Render. Los backups se generan con pg_dump desde GitHub Actions todos los días a las 02:00 UTC (21:00 en Perú).&lt;br&gt;
Antes de la prueba definimos dos objetivos, como se haría en una empresa:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Significado&lt;/th&gt;
&lt;th&gt;Objetivo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RPO (&lt;em&gt;Recovery Point Objective&lt;/em&gt;)&lt;/td&gt;
&lt;td&gt;Cuántos datos podemos perder como máximo&lt;/td&gt;
&lt;td&gt;24 h (un backup diario)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RTO (&lt;em&gt;Recovery Time Objective&lt;/em&gt;)&lt;/td&gt;
&lt;td&gt;Cuánto tiempo puede estar caído el servicio&lt;/td&gt;
&lt;td&gt;Menos de 15 minutos&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Empezar en local: un entorno reproducible&lt;/strong&gt;&lt;br&gt;
Antes de tocar producción ensayamos todo en local con Docker Compose, que levanta dos contenedores: PostgreSQL y la aplicación. Con un solo comando:&lt;br&gt;
docker compose up --build&lt;br&gt;
En local practicamos el ciclo completo: generar un backup dentro del contenedor, borrar la tabla, comprobar que la API respondía 503 y restaurar. Allí también probamos los casos negativos, que son los que dan confianza:&lt;br&gt;
• Restaurar un backup cifrado con una clave incorrecta falla y la base queda intacta, porque la restauración corre en una sola transacción.&lt;br&gt;
• Ejecutar restore.sh sin confirmación en un entorno no interactivo se niega a continuar.&lt;br&gt;
• El script verifica el checksum SHA-256 antes de tocar la base.&lt;br&gt;
Un detalle práctico: en mi equipo el puerto 5432 ya estaba ocupado por otro contenedor, así que publicamos la base local en el 5433 y lo dejamos configurable con una variable de entorno.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Restaurar con un clic&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Restaurar a mano exige descargar el artifact, descifrarlo y tener la cadena de conexión y la clave en la computadora. Durante una emergencia esos pasos son lentos y propensos a errores. Por eso creamos un workflow de restauración en GitHub Actions que usa los mismos secrets que el backup. Basta con escribir RESTAURAR como confirmación:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Hace un backup de seguridad del estado actual, por si se elige el backup equivocado.&lt;/li&gt;
&lt;li&gt; Descarga el artifact del último backup exitoso.&lt;/li&gt;
&lt;li&gt; Verifica el checksum, descifra y restaura en una sola transacción.&lt;/li&gt;
&lt;li&gt; Reporta las filas antes y después, y el tiempo de recuperación.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Primer intento: un falso positivo&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;En el primer ensayo en producción todo salió en verde... y aun así la prueba no demostraba nada. El resumen del workflow decía "Filas antes: 3", cuando acabábamos de borrar la tabla.&lt;br&gt;
Al investigar en los eventos de Render encontramos la causa. Al borrar la tabla, el health check de la aplicación empezó a responder 503, y Render reinició la instancia. Al arrancar, la aplicación ejecutaba init.sql, que recreaba la tabla e insertaba tres tareas de ejemplo. Como el backup contenía justo esas tres tareas, era imposible distinguir qué había recuperado el backup y qué había regenerado la aplicación.&lt;br&gt;
La corrección fue separar responsabilidades:&lt;br&gt;
• init.sql solo crea la estructura, sin datos. Si la tabla se pierde, la aplicación la recrea vacía.&lt;br&gt;
• Los datos de ejemplo pasaron a seed.sql, que solo se usa en el entorno local con Docker.&lt;br&gt;
Además, para la prueba definitiva creamos datos de control propios: "Tarea de Carlos" y "Tarea de Arturo".&lt;br&gt;
Esta fue para mí la lección más valiosa del proyecto: una prueba de recuperación debe diseñarse para poder fallar. Si el resultado sale bien pase lo que pase, la prueba no sirve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La prueba definitiva&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Estado inicial. La aplicación en producción con 5 tareas, incluidas las dos de control.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xqrg4rn3ldzxkz2orj0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3xqrg4rn3ldzxkz2orj0.png" alt=" " width="776" height="379"&gt;&lt;/a&gt;&lt;br&gt;
Figura 1. App con 5 tareas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Backup. Ejecutamos el workflow de backup a mano: 44 segundos, archivo cifrado de 4 KB y verificación automática con 5 filas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Desastre. En el SQL Editor de Neon ejecutamos: DROP TABLE tasks;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw259pe4sfiggm4l9mv23.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw259pe4sfiggm4l9mv23.png" alt=" " width="799" height="390"&gt;&lt;/a&gt;&lt;br&gt;
Figura 2. DROP TABLE en Neon.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Caída. La aplicación dejó de responder y Render mostró un 502 Bad Gateway.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjb66uhhqyil9z38eslj5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjb66uhhqyil9z38eslj5.png" alt=" " width="799" height="391"&gt;&lt;/a&gt;&lt;br&gt;
Figura 3. App caída.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pérdida de datos confirmada. Tras el reinicio automático, la aplicación volvió a funcionar pero con 0 tareas. Esta vez el desastre era visible.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbghulana8px9ydz4fhu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbghulana8px9ydz4fhu.png" alt=" " width="799" height="390"&gt;&lt;/a&gt;&lt;br&gt;
Figura 4. App vacía.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Restauración. Lanzamos el workflow de restauración. En 24 segundos la tabla pasó de 0 a 5 filas.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcun5l8bwwglu5hw3z3fa.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcun5l8bwwglu5hw3z3fa.png" alt=" " width="800" height="389"&gt;&lt;/a&gt;&lt;br&gt;
Figura 5. Restauración.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recuperación. Recargamos la aplicación: las 5 tareas volvieron, con sus identificadores originales (la "Tarea de Carlos" seguía siendo la #4 y la "Tarea de Arturo" la #5).&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0mrda7v4xmr3etpc3j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz0mrda7v4xmr3etpc3j4.png" alt=" " width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
Figura 6. App recuperada.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Resultados&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;Objetivo&lt;/th&gt;
&lt;th&gt;Resultado&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Datos perdidos (RPO)&lt;/td&gt;
&lt;td&gt;≤ 24 h&lt;/td&gt;
&lt;td&gt;0 registros&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo de restauración&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;24 s (18 s de recuperación efectiva)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo total de recuperación (RTO)&lt;/td&gt;
&lt;td&gt;&amp;lt; 15 min&lt;/td&gt;
&lt;td&gt;≈ 3 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El RPO fue cero porque no hubo escrituras entre el backup y el desastre. En un caso real perderíamos lo escrito desde el último backup diario: hasta 24 horas de datos. Para reducirlo bastaría con ejecutar el backup con más frecuencia o aprovechar la recuperación a un punto en el tiempo (PITR) que ofrece Neon.&lt;br&gt;
El tiempo total incluye detectar la caída y esperar que Render reinicie la instancia; la restauración en sí fue cuestión de segundos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusiones&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;• Medir convierte un backup en una estrategia. Sin RPO y RTO definidos de antemano no hay forma de saber si la recuperación fue "suficientemente rápida".&lt;br&gt;
• Los datos de control hacen la prueba honesta. Registros identificables como "Tarea de Arturo" permiten afirmar con certeza que los datos vinieron del backup.&lt;br&gt;
• La restauración también se automatiza. En una emergencia nadie quiere copiar cadenas de conexión a mano; un workflow con confirmación reduce el error humano.&lt;br&gt;
• Respaldar antes de restaurar. Restaurar el backup equivocado es otro desastre; el backup de seguridad previo lo evita.&lt;/p&gt;

&lt;p&gt;Como mejora futura propondría repetir esta prueba de desastre de forma periódica (por ejemplo, una vez al mes) y registrar el RTO en cada ejecución para detectar si el tiempo de recuperación empeora a medida que crece la base.&lt;br&gt;
El código, los workflows y todas las capturas están en el repositorio. El desarrollo fue grupal; este artículo presenta mi análisis individual de la prueba de recuperación.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>github</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
