<?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: Takiuddin Ahmed</title>
    <description>The latest articles on DEV Community by Takiuddin Ahmed (@takiuddinahmed).</description>
    <link>https://dev.to/takiuddinahmed</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%2F392830%2Fe5adee12-c459-4f07-ba01-b563178461ab.jpeg</url>
      <title>DEV Community: Takiuddin Ahmed</title>
      <link>https://dev.to/takiuddinahmed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/takiuddinahmed"/>
    <language>en</language>
    <item>
      <title>A backup you haven't restored isn't a backup</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Sun, 09 Aug 2026 18:00:14 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/a-backup-you-havent-restored-isnt-a-backup-4lj</link>
      <guid>https://dev.to/takiuddinahmed/a-backup-you-havent-restored-isnt-a-backup-4lj</guid>
      <description>&lt;p&gt;Migrating from MongoDB Atlas to a self-hosted replica set bought us control and cut our bill. It also quietly removed something we had stopped thinking about: Atlas had been taking continuous backups for us the entire time.&lt;/p&gt;

&lt;p&gt;After the migration, production data for &lt;a href="https://prochesta.app/" rel="noopener noreferrer"&gt;Prochesta&lt;/a&gt; lived in &lt;code&gt;/var/db/mongo&lt;/code&gt; on a single VPS. No snapshots. No off-box copy. A &lt;code&gt;rm -rf&lt;/code&gt;, a bad migration script, or a dead disk would have been the end of it. We had written "backups" as a follow-up task in the migration spec, which is the engineering equivalent of a sticky note on a bank vault.&lt;/p&gt;

&lt;p&gt;The requirement we actually cared about was narrower than "back up the database". Most real-world data loss at our scale isn't hardware failure — it's a deploy that writes garbage, or someone running an update without a filter. Recovering to &lt;em&gt;last night&lt;/em&gt; doesn't help when the damage happened at 14:20 and you noticed at 14:50. We needed to recover to &lt;strong&gt;an arbitrary moment&lt;/strong&gt;, not to a nightly snapshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint nobody mentions: Community has no &lt;code&gt;$backupCursor&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We chose &lt;a href="https://docs.percona.com/percona-backup-mongodb/" rel="noopener noreferrer"&gt;Percona Backup for MongoDB&lt;/a&gt; (PBM), and immediately hit the limitation that shapes every decision downstream.&lt;/p&gt;

&lt;p&gt;PBM offers &lt;em&gt;physical&lt;/em&gt; backups — fast file-level copies that restore in minutes and barely touch the running server. They work by opening a backup cursor via the &lt;code&gt;$backupCursor&lt;/code&gt; aggregation stage. That stage exists in Percona Server for MongoDB and in MongoDB Enterprise. It does &lt;strong&gt;not&lt;/strong&gt; exist in MongoDB Community, which is what the official &lt;code&gt;mongo:8.0&lt;/code&gt; image ships.&lt;/p&gt;

&lt;p&gt;So on Community, PBM gives you logical backups only: every document read out through &lt;code&gt;mongod&lt;/code&gt;, compressed, and shipped off-box. Two consequences, both accepted deliberately rather than discovered later:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backups cost CPU on the primary — and with a single-member replica set there's no secondary to offload the read to.&lt;/li&gt;
&lt;li&gt;Restores insert documents and rebuild indexes, so restore time grows with data size much faster than backup time does.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At our current size that's minutes, not hours. It's also the thing that will eventually justify swapping the image for Percona Server. Knowing &lt;em&gt;which&lt;/em&gt; constraint will force the next migration is worth more than pretending there isn't one.&lt;/p&gt;

&lt;h2&gt;
  
  
  We had already built one of these. We didn't use it.
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/takiuddinahmed/mongopit" rel="noopener noreferrer"&gt;mongopit&lt;/a&gt;, a self-hosted MongoDB point-in-time recovery tool. Its architecture is the same shape as what we ended up with: &lt;code&gt;mongodump --oplog&lt;/code&gt; for consistent full snapshots, a daemon tailing the oplog and streaming compressed BSON segments to cloud storage, and &lt;code&gt;mongorestore --oplogReplay --oplogLimit&lt;/code&gt; to stop the replay just before the operation that ruined your day. It ships GFS retention, a &lt;code&gt;--dry-run&lt;/code&gt; restore preview, and runs as a single Docker Compose service.&lt;/p&gt;

&lt;p&gt;It stores backups on &lt;strong&gt;Google Drive via rclone&lt;/strong&gt;. That's the reason we reached for PBM instead — we wanted Cloudflare R2 as a first-class target, and the reasons are worth spelling out because they aren't really about Drive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The data was already there.&lt;/strong&gt; Prochesta serves user uploads, receipts and generated PDFs from R2 through the S3 SDK. Same account, same credentials, same billing, same dashboard we already check during an incident. Adding Google Drive meant a second provider, a second service account, and a second thing to be wrong at 3am.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;R2 has zero egress fees, and that's a reliability feature, not a line item.&lt;/strong&gt; Restore rehearsals pull the entire backup down. If every drill has a bandwidth cost attached, drills quietly stop happening — and an unrehearsed backup is a rumour. We wanted the cost of practising to be zero so there's never a reason to skip it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Object storage semantics beat file-sync semantics for this job.&lt;/strong&gt; S3-style multipart uploads, predictable API rate limits, and a flat key namespace are what an unattended nightly job wants. Drive service accounts bring their own operational texture: daily transfer ceilings, the shared-drive requirement for service accounts, and API quota errors that surface as a failed backup rather than a slow one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PBM speaks S3 natively&lt;/strong&gt;, so R2 needs configuration, not a translation layer. rclone's S3 backend could in principle point mongopit at R2 — but that's a path the tool wasn't built or tested around, and we'd be maintaining it ourselves.&lt;/p&gt;

&lt;p&gt;None of this makes mongopit the wrong choice. If Drive is where your storage budget already lives, or you want something small enough to read end to end in an afternoon, it does the job and it does point-in-time properly. Ours was a storage decision, not a verdict on the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually runs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────── VPS ─────────────┐
│  mongod (rs0, 1 member)      │
│      ▲                       │
│      │ logical read          │
│  pbm-agent ──── full backup daily 20:00 ────┐
│      └───────── oplog slice / 10 min ───────┤
│                              │              ▼
│  host cron                   │      Cloudflare R2
│   ├─ backup  20:00           │      one bucket
│   └─ metrics every 5 min     │      ├── prod/
│         │                    │      └── dev/
│         ▼                    │
│  pbm-metrics.sh              │
│   (pbm status -o json → .prom)
│         │                    │
│         ▼                    │
│  node_exporter → Prometheus → Alertmanager → email
└──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One bucket, two prefixes, one guard
&lt;/h2&gt;

&lt;p&gt;Dev and prod write into the same R2 bucket, separated by a key prefix. One bucket means one token, one lifecycle policy, and one place to look. It also means a misconfigured dev box could write into production's backup path — and, worse, &lt;em&gt;prune&lt;/em&gt; it, since retention deletes anything older than the window.&lt;/p&gt;

&lt;p&gt;So the bootstrap script doesn't trust the config file it just sent. It asks PBM what it actually stored, and refuses to continue if the prefix isn't the one this box is supposed to own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;applied_prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pbm_config_value storage.s3.prefix&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$applied_prefix&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PBM_R2_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; die &lt;span class="s2"&gt;"..."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuration that verifies itself is the difference between a safety property and a naming convention.&lt;/p&gt;

&lt;p&gt;The R2 specifics, for anyone doing the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;storage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;s3&lt;/span&gt;
  &lt;span class="na"&gt;s3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;auto&lt;/span&gt;          &lt;span class="c1"&gt;# R2's only valid region&lt;/span&gt;
    &lt;span class="na"&gt;endpointUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://&amp;lt;ACCOUNT_ID&amp;gt;.r2.cloudflarestorage.com&lt;/span&gt;
    &lt;span class="na"&gt;forcePathStyle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;  &lt;span class="c1"&gt;# PBM's default; pinned so it stays true&lt;/span&gt;
    &lt;span class="na"&gt;bucket&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prochesta-mongo-backups&lt;/span&gt;
    &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod&lt;/span&gt;          &lt;span class="c1"&gt;# or dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Seven days of snapshots, two days of moments
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Retention&lt;/th&gt;
&lt;th&gt;Granularity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full backups&lt;/td&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;one per night at 20:00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oplog slices&lt;/td&gt;
&lt;td&gt;2 days&lt;/td&gt;
&lt;td&gt;every 10 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Inside two days we can restore to any instant — 14:19:59, one second before the bad write. Beyond that we have nightly snapshots for a week. Corruption discovered on day three costs up to 24 hours of data; corruption discovered within two days costs seconds.&lt;/p&gt;

&lt;p&gt;The two windows are coupled in a way that's easy to get wrong. Point-in-time recovery replays oplog slices &lt;em&gt;on top of a full backup&lt;/em&gt;. If the oplog window were shorter than the interval between backups, the newest snapshot would have no slices behind it and PITR would silently degrade to "restore last night". Our backup script refuses to run if that invariant is ever violated in config.&lt;/p&gt;

&lt;h2&gt;
  
  
  The dead man's switch
&lt;/h2&gt;

&lt;p&gt;Percona ships a PBM collector for &lt;code&gt;mongodb_exporter&lt;/code&gt;. We deliberately didn't use it.&lt;/p&gt;

&lt;p&gt;It reports what PBM's own collections say — agent status, backup sizes, transition timestamps. All useful. But consider how a backup system actually fails in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cron entry gets wiped during a host rebuild.&lt;/li&gt;
&lt;li&gt;The pruning step starts erroring while the backup itself still succeeds.&lt;/li&gt;
&lt;li&gt;Someone runs a restore, which turns PITR off, and nobody re-enables it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In every one of those cases the exporter keeps reporting a healthy last backup, because the last backup &lt;em&gt;was&lt;/em&gt; healthy. It just happened eleven days ago.&lt;/p&gt;

&lt;p&gt;Instead, one script parses &lt;code&gt;pbm status -o json&lt;/code&gt; into a Prometheus textfile every five minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;prochesta_pbm_metrics_scrape_ts&lt;/span&gt;       &lt;span class="mi"&gt;1754337900&lt;/span&gt;
&lt;span class="n"&gt;prochesta_pbm_last_backup_success_ts&lt;/span&gt;  &lt;span class="mi"&gt;1754337005&lt;/span&gt;
&lt;span class="n"&gt;prochesta_pbm_pitr_last_chunk_ts&lt;/span&gt;      &lt;span class="mi"&gt;1754337600&lt;/span&gt;
&lt;span class="n"&gt;prochesta_pbm_agents_ok&lt;/span&gt;               &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That first timestamp is the whole idea. If the monitoring itself dies, the value stops moving and &lt;code&gt;PBMMetricsStale&lt;/code&gt; fires within fifteen minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;alert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PBMMetricsStale&lt;/span&gt;
  &lt;span class="na"&gt;expr&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;absent(prochesta_pbm_metrics_scrape_ts)&lt;/span&gt;
        &lt;span class="s"&gt;or (time() - prochesta_pbm_metrics_scrape_ts) &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;900&lt;/span&gt;
  &lt;span class="na"&gt;for&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, a dead cron job is indistinguishable from a healthy system: no errors, no alerts, no backups. The runbook says to check that alert first, because while it's firing every other backup metric is frozen at a stale value and telling you nothing about now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The script always writes its file — especially when everything is broken.&lt;/strong&gt; If PBM is unreachable it writes &lt;code&gt;prochesta_pbm_up 0&lt;/code&gt; and moves on. Going silent on failure would be the one behaviour that defeats the entire purpose.&lt;/p&gt;

&lt;p&gt;Twelve rules sit on top of those metrics. The one we expect to earn its keep least often is the most interesting: &lt;code&gt;PBMBackupSizeCollapsed&lt;/code&gt; fires when a backup succeeds but comes in at under half its seven-day average — the shape of a backup that completed and is missing most of the data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two traps in the restore path
&lt;/h2&gt;

&lt;p&gt;A logical restore can't run against a cluster that's being written to. So restores are a downtime event, and the script owns that explicitly: it stops the API container, restores, and brings it back on &lt;em&gt;every&lt;/em&gt; exit path — a failed restore must not also leave the site down.&lt;/p&gt;

&lt;p&gt;The second trap is nastier. &lt;strong&gt;A restore turns PITR off&lt;/strong&gt;, and the restored state has no full backup behind it. Point-in-time coverage is gone, but &lt;code&gt;pbm status&lt;/code&gt; still cheerfully lists all the older backups, so nothing looks wrong. Re-enabling PITR and taking a fresh base backup is therefore built into the restore script as its final step, not written down as something an operator is expected to remember at the end of an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things that broke
&lt;/h2&gt;

&lt;p&gt;Everything above validated cleanly — compose files parsed, &lt;code&gt;promtool&lt;/code&gt; passed, the metrics parser was tested against fixtures built from PBM's own source. Then we ran it on a real box.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The deploy started an agent that couldn't authenticate.&lt;/strong&gt; Our deploy script ends with &lt;code&gt;docker compose up -d --wait&lt;/code&gt;. On a box that had never been through backup setup, that started &lt;code&gt;pbm-agent&lt;/code&gt; with an empty password. It failed its healthcheck, went unhealthy, and failed the entire deploy — on a host that simply wasn't using backups yet. Fix: put the backup services behind a compose profile so the default service set excludes them entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Setup succeeded without doing anything.&lt;/strong&gt; The setup script preserved existing values in &lt;code&gt;.env&lt;/code&gt;, which is correct for passwords and wrong for a feature flag. With &lt;code&gt;PBM_ENABLED=false&lt;/code&gt; already set, every subsequent step no-opped, the verification backup exited zero without backing anything up, and the closing report announced that backups were configured. Running the setup script &lt;em&gt;is&lt;/em&gt; the request to enable backups, so it now forces that value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. PBM returns config values in brackets.&lt;/strong&gt; Our prefix guard compared &lt;code&gt;pbm config storage.s3.bucket&lt;/code&gt; against the expected name. PBM 2.15 returns &lt;code&gt;[storage.s3.bucket=prochesta-mongo-backups]&lt;/code&gt;, not a bare value, so the comparison never matched and bootstrap aborted on a box where the configuration had applied perfectly. The same bad parse silently reset PITR on every run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The restore prompted into a pipe.&lt;/strong&gt; &lt;code&gt;pbm restore&lt;/code&gt; asks "Are you sure?" before dispatching. We invoke it through &lt;code&gt;docker compose exec -T&lt;/code&gt;, which allocates no TTY, so it died with &lt;code&gt;Error: no tty&lt;/code&gt; — after our script had already stopped the API. The confirmation happens before the restore is dispatched, so nothing was touched, but our error message claimed the database was "in whatever state the restore reached". The fix is one flag. The lesson is that a restore path you have never executed is not a restore path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 517 MB of database is a 75 MB backup
&lt;/h2&gt;

&lt;p&gt;The first production backup came in at 74.98 MB against a data directory of 517 MB, which is exactly the kind of number that makes you want to verify before trusting. It decomposes cleanly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;In a logical backup?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WiredTiger journal&lt;/td&gt;
&lt;td&gt;201 MB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oplog (&lt;code&gt;local&lt;/code&gt; db)&lt;/td&gt;
&lt;td&gt;120 MB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagnostic data&lt;/td&gt;
&lt;td&gt;8.8 MB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Collections + indexes on disk&lt;/td&gt;
&lt;td&gt;184 MB&lt;/td&gt;
&lt;td&gt;Documents only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data directory total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;517 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;→ 74.98 MB shipped&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A logical backup carries documents, not storage. Indexes are rebuilt at restore time from their definitions rather than copied. The journal and oplog are working state. What remains is BSON compressed with s2 — and 184 MB of on-disk collections and indexes reducing to 75 MB is precisely what you'd expect.&lt;/p&gt;

&lt;p&gt;Arithmetic like this rules out the blunt failure — a backup that ran and captured almost nothing. It proves nothing about whether the backup restores.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we haven't solved
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automated restore verification doesn't exist yet.&lt;/strong&gt; Nothing on a schedule restores the latest backup into a throwaway instance and asserts that collection counts match. Until that exists, every backup is unproven except the ones a human has personally drilled. This is the single highest-value thing left, and &lt;code&gt;PBMBackupSizeCollapsed&lt;/code&gt; is a deliberately crude stand-in for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The backups share the application's R2 credentials.&lt;/strong&gt; Anything holding that key — including the running API container — can delete every backup, and R2 offers no object versioning or object lock to fall back on. We took this knowingly to limit secret sprawl, and built the seam for the fix: dedicated &lt;code&gt;PBM_R2_*&lt;/code&gt; keys take precedence over the shared ones, so moving to a bucket-scoped token is three lines of config and no code change. Naming the risk in the spec isn't the same as removing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two days of point-in-time is short.&lt;/strong&gt; Corruption noticed on day three falls back to a nightly snapshot. Widening it costs only R2 storage — we start narrow and widen when something teaches us to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for the failure you'll actually have
&lt;/h2&gt;

&lt;p&gt;The interesting decisions here weren't about backup software. They were about which failure we believed would happen.&lt;/p&gt;

&lt;p&gt;We assumed the cron would silently stop long before we assumed the disk would die, so we built a dead man's switch before we built a second storage region. We assumed someone would eventually run a restore against the wrong environment, so the prefix guard aborts rather than warns. We assumed drills would stop happening if they had a cost, so we chose storage with zero egress.&lt;/p&gt;

&lt;p&gt;Every one of the four rollout bugs was in the operational scaffolding — the deploy interaction, the flag handling, the output parsing, the TTY — and not one was in the backup engine. That's where these systems break, and it's why "we configured backups" and "we have backups" are different sentences.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built at &lt;a href="https://prochesta.app/" rel="noopener noreferrer"&gt;Prochesta&lt;/a&gt; — an exam preparation platform for students in Bangladesh, with model tests, live exams, friendly battles and performance tracking. The infrastructure above is what keeps their attempt history, results and subscriptions safe.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And if Google Drive is where your backups belong, take a look at &lt;a href="https://github.com/takiuddinahmed/mongopit" rel="noopener noreferrer"&gt;mongopit&lt;/a&gt; — self-hosted MongoDB point-in-time recovery, oplog replay and GFS retention, in one Compose service.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>devops</category>
      <category>mongodb</category>
    </item>
    <item>
      <title>Nightly backups aren’t enough when someone drops the wrong MongoDB collection. I built an open-source PITR system using oplog replay, Docker, and Google Drive - so you can restore to the second before disaster. Feedback welcome!</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Fri, 24 Jul 2026 13:14:33 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/nightly-backups-arent-enough-when-someone-drops-the-wrong-mongodb-collection-i-built-an-3igl</link>
      <guid>https://dev.to/takiuddinahmed/nightly-backups-arent-enough-when-someone-drops-the-wrong-mongodb-collection-i-built-an-3igl</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51" class="crayons-story__hidden-navigation-link"&gt;How I Built a MongoDB Backup System That Can Undo an Accidental Drop&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/takiuddinahmed" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F392830%2Fe5adee12-c459-4f07-ba01-b563178461ab.jpeg" alt="takiuddinahmed profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/takiuddinahmed" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Takiuddin Ahmed
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Takiuddin Ahmed
                
              
              &lt;div id="story-author-preview-content-4223173" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/takiuddinahmed" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F392830%2Fe5adee12-c459-4f07-ba01-b563178461ab.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Takiuddin Ahmed&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 24&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51" id="article-link-4223173"&gt;
          How I Built a MongoDB Backup System That Can Undo an Accidental Drop
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mongodb"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mongodb&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/backup"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;backup&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/docker"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;docker&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            7 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>database</category>
      <category>docker</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a MongoDB Backup System That Can Undo an Accidental Drop</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Fri, 24 Jul 2026 10:58:18 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51</link>
      <guid>https://dev.to/takiuddinahmed/how-i-built-a-mongodb-backup-system-that-can-undo-an-accidental-drop-2l51</guid>
      <description>&lt;p&gt;Every developer has the same nightmare. Someone runs a command on the wrong database, a collection gets dropped, and suddenly the data is just gone. Your last backup was from midnight, so you can restore that, but you still lose everything that happened after midnight.&lt;/p&gt;

&lt;p&gt;I wanted something better. Not just "restore last night's backup" but "restore the database to the exact second before the drop happened." That feature has a name: point-in-time recovery, or PITR. This post is about how I built it, and the one small MongoDB fact that makes the whole thing possible.&lt;/p&gt;

&lt;p&gt;The project is open source and called &lt;code&gt;mongopit&lt;/code&gt;. You can find it here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/takiuddinahmed/mongopit" rel="noopener noreferrer"&gt;https://github.com/takiuddinahmed/mongopit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The core problem
&lt;/h2&gt;

&lt;p&gt;A normal backup is a photo. You take one at midnight, another at noon, and if something goes wrong at 11am you are stuck with the midnight photo. Everything between midnight and 11am is lost.&lt;/p&gt;

&lt;p&gt;Point-in-time recovery is more like a video. You keep the midnight photo, but you also record every single change after it. When disaster strikes, you can play the video forward and stop it one frame before the bad thing happened.&lt;/p&gt;

&lt;p&gt;In MongoDB, that "video" already exists. It is called the oplog.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one fact that changes everything
&lt;/h2&gt;

&lt;p&gt;The oplog (operations log) is an ordered record of every write MongoDB makes. Every insert, update, and delete goes into it. If you have the oplog, you can replay history.&lt;/p&gt;

&lt;p&gt;Here is the catch that surprises a lot of people. A standalone MongoDB server has no oplog. The oplog only exists on replica set members. So if you are running a plain single MongoDB, you cannot do point-in-time recovery at all, no matter what backup tool you use.&lt;/p&gt;

&lt;p&gt;The fix is simple and it does not need extra servers. You run your MongoDB as a single-node replica set. It is still one machine, one process, one data directory. You just turn replication on. That one change gives you an oplog, and the oplog gives you PITR.&lt;/p&gt;

&lt;p&gt;This is the reason the whole system is shaped the way it is. If you ever see the setup and think "why not just run it standalone," this is why.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the recovery actually works
&lt;/h2&gt;

&lt;p&gt;The system has two things running at all times.&lt;/p&gt;

&lt;p&gt;First, a scheduled full backup. This uses &lt;code&gt;mongodump --oplog&lt;/code&gt; to take a consistent snapshot of everything. Before the dump, it records the current oplog position. Think of this as the photo, plus a note saying "this photo was taken at frame 5000."&lt;/p&gt;

&lt;p&gt;Second, a continuous oplog tailer. This is a small process that watches the oplog and keeps copying new operations into files. It runs all the time, flushing every few seconds. This is the video recording that never stops.&lt;/p&gt;

&lt;p&gt;Both of these get compressed and shipped off to storage.&lt;/p&gt;

&lt;p&gt;When you need to recover, three steps happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the most recent full backup taken before your target time. Restore it. Now you have the database as it was at the photo.&lt;/li&gt;
&lt;li&gt;Gather all the recorded oplog operations between the photo and your target time.&lt;/li&gt;
&lt;li&gt;Replay those operations, but tell MongoDB to stop just before the target timestamp.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last part is the magic. &lt;code&gt;mongorestore&lt;/code&gt; has a flag called &lt;code&gt;--oplogLimit&lt;/code&gt;, and it is exclusive. Any operation at or after that timestamp is skipped. So if you set the limit to the exact timestamp of the drop, the replay stops right before it. The drop never happens in your restored copy. Everything up to that instant is saved.&lt;/p&gt;

&lt;p&gt;To find that exact timestamp, the tool has an &lt;code&gt;inspect&lt;/code&gt; command that scans the oplog and shows you destructive operations like drops and deletes, with the precise timestamp to stop before. You copy that timestamp, paste it into the restore command, and you are done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why replaying the same data twice is safe
&lt;/h2&gt;

&lt;p&gt;You might notice a small overlap. The full backup already contains some oplog data, and then we replay external oplog on top of it. Are we not applying some operations twice?&lt;/p&gt;

&lt;p&gt;Yes, and it does not matter. MongoDB's oplog replay is idempotent. Applying the same operation twice gives the same result as applying it once. Inserts become upserts, and so on. This is a nice property because it means restarts and overlaps never corrupt the result. It also means I did not have to write clever code to track exactly-once delivery, which is where bugs love to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the backups go
&lt;/h2&gt;

&lt;p&gt;I wanted to store backups on Google Drive, mostly because it is cheap and easy for a small setup. No mainstream backup tool supports Google Drive directly, so I used &lt;code&gt;rclone&lt;/code&gt;, which speaks to dozens of storage backends including Drive.&lt;/p&gt;

&lt;p&gt;Everything that touches storage goes through one thin wrapper around the &lt;code&gt;rclone&lt;/code&gt; command line, with retries and backoff built in. There is no separate database or index tracking what backups exist. The storage itself is the source of truth. To figure out what is available, the system just lists the files. Full backups live in one folder, oplog segments in another, and the file names carry the timestamps. Fewer moving parts means fewer things that can drift out of sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I learned building it
&lt;/h2&gt;

&lt;p&gt;Timestamps show up in three different shapes. One format for file names, one for the restore command line, and one for the metadata files. I kept getting them mixed up until I wrote small helper functions for each and used them everywhere. If you deal with MongoDB timestamps, do this early.&lt;/p&gt;

&lt;p&gt;Retention is easy to get wrong in a dangerous way. I used a grandfather-father-son policy, keeping some daily, weekly, and monthly backups. The important rule is that you must never delete oplog segments if they still have no full backup to sit on top of. An oplog segment with no base to apply onto is useless. Prune in the wrong order and you quietly destroy your ability to recover.&lt;/p&gt;

&lt;p&gt;A backup you have never restored is not a backup. It is a hope. So the system has a verify step that spins up a throwaway MongoDB container, restores the latest backup into it, checks that the data is actually there, and then throws the container away. If that ever fails, you find out on a normal Tuesday instead of during a real emergency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole thing runs in Docker
&lt;/h2&gt;

&lt;p&gt;Everything ships as a Docker Compose stack. There is the MongoDB container running as a single-node replica set, and one backup agent container that does three jobs in one process: it makes sure the replica set is initiated on startup, it runs the oplog tailer in a background thread, and it runs the scheduler for full backups, retention, and verification. I started with three separate services and combined them into one. The memory savings are modest, but the simplicity is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to actually use it
&lt;/h2&gt;

&lt;p&gt;Here is what running the tool looks like in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 0: Get it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Clone the repository and you have everything you need. It ships as a Docker Compose stack, so you do not have to install Python or MongoDB tools by hand.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/takiuddinahmed/mongopit.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mongopit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 1: Point it at your MongoDB and storage.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Everything is driven by a &lt;code&gt;.env&lt;/code&gt; file and an &lt;code&gt;rclone.conf&lt;/code&gt;. You tell it your MongoDB connection string, your rclone remote (like Google Drive), how often to run full backups, and how many daily, weekly, and monthly copies to keep.&lt;/p&gt;

&lt;p&gt;If your MongoDB is currently standalone, you do a one-time conversion to a single-node replica set. Stop it, start it again with &lt;code&gt;--replSet rs0&lt;/code&gt;, connect with &lt;code&gt;mongosh&lt;/code&gt;, and run this once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initiate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rs0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;members&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;yourhost:27017&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your data stays exactly where it is. The oplog starts recording from that moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Start the stack.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That brings up MongoDB and a single backup agent. The agent initiates the replica set if needed, starts recording the oplog, and schedules your full backups, cleanup, and verification. From here it runs on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Take a backup now if you want one.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You do not have to wait for the schedule. You can trigger a full backup any time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup-agent mongopit backup-full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: When something bad happens, find the moment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Say a collection got dropped. First you look at the recent history to find the exact timestamp of the drop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup-agent mongopit inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This prints recent destructive operations with a value called &lt;code&gt;oplogLimit&lt;/code&gt; next to each one. That value is the timestamp you will recover up to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Recover to just before it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take the timestamp from the previous step and pass it to the restore command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup-agent mongopit restore &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target-uri&lt;/span&gt; &lt;span class="s2"&gt;"mongodb://mongo:27017/?replicaSet=rs0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--point-in-time&lt;/span&gt; 1721800020:1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--drop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the limit is exclusive, the drop is never replayed. The collection comes back with every document that existed the instant before it was dropped.&lt;/p&gt;

&lt;p&gt;A good habit: run it with &lt;code&gt;--dry-run&lt;/code&gt; first. That prints the plan, which base backup and which oplog segments it will use, without touching any data. When you are happy, run it for real. If you are nervous about production, restore into a scratch database first and check it there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other commands you will use.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# apply retention now instead of waiting for the schedule&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup-agent mongopit prune

&lt;span class="c"&gt;# prove your latest backup actually restores&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;backup-agent mongopit verify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;verify&lt;/code&gt; command is the one I recommend running on a schedule and not just once. It restores your latest backup into a throwaway container, checks the data is really there, and cleans up after itself. That is how you find out a backup is broken on a calm day rather than during a real incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying the recovery drill
&lt;/h2&gt;

&lt;p&gt;The most satisfying test was the drop recovery drill. Seed some documents, take a full backup, add a few more documents, then drop the collection on purpose. Run &lt;code&gt;inspect&lt;/code&gt; to find the drop timestamp. Run &lt;code&gt;restore&lt;/code&gt; with that timestamp. Then count the documents. Every single one is back, and the drop is gone, as if it never happened.&lt;/p&gt;

&lt;p&gt;The first time that worked exactly as planned, it felt like undoing a mistake in real life. That is the feeling I was chasing when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;The interesting part of this project was not the code. It was realizing that MongoDB already keeps a complete recording of every change, and that the only thing standing between you and time travel is turning on replication and capturing that recording before it rolls away.&lt;/p&gt;

&lt;p&gt;If you run MongoDB and you are relying on nightly snapshots alone, I would gently suggest looking into point-in-time recovery. The day you need it, you will be very glad it is there.&lt;/p&gt;

&lt;p&gt;The tool is open source and free to use. If you want to try it, star it, or open an issue, it lives here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/takiuddinahmed/mongopit" rel="noopener noreferrer"&gt;https://github.com/takiuddinahmed/mongopit&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You can find more of my work and writing on my website: &lt;strong&gt;&lt;a href="https://takiuddin.me" rel="noopener noreferrer"&gt;https://takiuddin.me&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>backup</category>
      <category>docker</category>
      <category>python</category>
    </item>
    <item>
      <title>Keep Cursor IDE Updated Automatically on Linux with cursor-updater</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Wed, 17 Dec 2025 22:17:59 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/keep-cursor-ide-updated-automatically-on-linux-with-cursor-updater-2dm0</link>
      <guid>https://dev.to/takiuddinahmed/keep-cursor-ide-updated-automatically-on-linux-with-cursor-updater-2dm0</guid>
      <description>&lt;p&gt;If you use &lt;strong&gt;Cursor IDE&lt;/strong&gt; on Linux, you’ve probably noticed that updates aren’t exactly smooth. Cursor ships as an AppImage, which means updates usually involve downloading a new file and replacing the old one manually.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;&lt;code&gt;cursor-updater&lt;/code&gt;&lt;/strong&gt;, a small, reliable Linux updater that keeps Cursor up to date automatically using Cursor’s &lt;strong&gt;official download API&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post explains &lt;strong&gt;why it exists&lt;/strong&gt;, &lt;strong&gt;how it works&lt;/strong&gt;, and &lt;strong&gt;how to set it up in minutes&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Cursor IDE works great on Linux, but updating it usually means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manually checking for a new version&lt;/li&gt;
&lt;li&gt;Downloading a new AppImage&lt;/li&gt;
&lt;li&gt;Replacing the old file&lt;/li&gt;
&lt;li&gt;Fixing permissions&lt;/li&gt;
&lt;li&gt;Updating symlinks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That’s fine once or twice—but annoying if you use Cursor daily and want to stay current.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cursor-updater&lt;/code&gt;&lt;/strong&gt; is a simple Bash-based updater that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Fetches the latest Cursor release via the &lt;strong&gt;official API&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Downloads and installs the AppImage automatically&lt;/li&gt;
&lt;li&gt;✅ Creates timestamped backups before updating&lt;/li&gt;
&lt;li&gt;✅ Manages symlinks cleanly&lt;/li&gt;
&lt;li&gt;✅ Supports &lt;strong&gt;stable&lt;/strong&gt; and &lt;strong&gt;insiders&lt;/strong&gt; tracks&lt;/li&gt;
&lt;li&gt;✅ Can run automatically using &lt;strong&gt;systemd timers&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No GUI. No Electron wrapper. Just a tool that does one job well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;h3&gt;
  
  
  One-Line Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/takiuddinahmed/cursor-updater/main/scripts/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;update-cursor&lt;/code&gt; to &lt;code&gt;/usr/local/bin&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Optionally install a systemd service + timer for auto-updates&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Manual Install
&lt;/h3&gt;

&lt;p&gt;If you prefer inspecting the code first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/takiuddinahmed/cursor-updater.git
&lt;span class="nb"&gt;cd &lt;/span&gt;cursor-updater
./scripts/install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual Update
&lt;/h3&gt;

&lt;p&gt;Update to the latest &lt;strong&gt;stable&lt;/strong&gt; release:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-cursor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update to the &lt;strong&gt;insiders&lt;/strong&gt; track:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;update-cursor insiders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens internally:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Detects CPU architecture (&lt;code&gt;x86_64&lt;/code&gt; or &lt;code&gt;ARM64&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Calls Cursor’s official download API&lt;/li&gt;
&lt;li&gt;Resolves the correct AppImage URL&lt;/li&gt;
&lt;li&gt;Creates a timestamped backup&lt;/li&gt;
&lt;li&gt;Installs the new AppImage to &lt;code&gt;/opt/cursor/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;/usr/local/bin/cursor&lt;/code&gt; symlink&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Automatic Updates (systemd)
&lt;/h2&gt;

&lt;p&gt;Enable daily updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; cursor-update.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check timer status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl status cursor-update.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; cursor-update.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Want weekly updates instead?&lt;br&gt;
Edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/etc/systemd/system/cursor-update.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;daily&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="py"&gt;OnCalendar&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;weekly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How It Works (Simplified)
&lt;/h2&gt;

&lt;p&gt;At its core, the updater:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects platform&lt;/li&gt;
&lt;li&gt;Queries Cursor’s download API&lt;/li&gt;
&lt;li&gt;Parses the JSON response&lt;/li&gt;
&lt;li&gt;Downloads the AppImage safely&lt;/li&gt;
&lt;li&gt;Replaces the existing installation with a backup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Core logic excerpt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ARCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;x86_64&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;PLATFORM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"linux-x64"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  aarch64|arm64&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;PLATFORM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"linux-arm64"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;

&lt;span class="nv"&gt;API_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://www.cursor.com/api/download?platform=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PLATFORM&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;releaseTrack=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TRACK&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="nt"&gt;--retry&lt;/span&gt; 5 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;DOWNLOAD_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;parse_json &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$JSON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
curl &lt;span class="nt"&gt;-fL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TMP_APP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DOWNLOAD_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JSON Parsing Note
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Uses &lt;code&gt;python3&lt;/code&gt; if available (recommended)&lt;/li&gt;
&lt;li&gt;Falls back to &lt;code&gt;sed/grep&lt;/code&gt; for minimal systems&lt;/li&gt;
&lt;li&gt;Includes validation so empty or invalid API responses fail safely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero config&lt;/strong&gt; after install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic backups&lt;/strong&gt; with timestamps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stable &amp;amp; insiders&lt;/strong&gt; support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;x86_64 + ARM64&lt;/strong&gt; support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;systemd timer integration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear error messages and retries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal dependencies&lt;/strong&gt; (&lt;code&gt;curl&lt;/code&gt;, optional &lt;code&gt;python3&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  File Locations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Updater: &lt;code&gt;/usr/local/bin/update-cursor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;AppImage: &lt;code&gt;/opt/cursor/cursor.AppImage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Symlink: &lt;code&gt;/usr/local/bin/cursor&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;systemd units:
&lt;code&gt;/etc/systemd/system/cursor-update.{service,timer}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Uninstall
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/uninstall.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes the updater and optionally disables the timer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I use Cursor daily on Arch Linux. Manually updating an AppImage every few weeks breaks flow—and eventually gets skipped.&lt;/p&gt;

&lt;p&gt;I wanted something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;predictable&lt;/li&gt;
&lt;li&gt;transparent&lt;/li&gt;
&lt;li&gt;easy to audit&lt;/li&gt;
&lt;li&gt;friendly to Linux conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;code&gt;cursor-updater&lt;/code&gt;. Nothing fancy. Just reliable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;Issues, pull requests, and ideas are welcome:&lt;br&gt;
👉 &lt;a href="https://github.com/takiuddinahmed/cursor-updater" rel="noopener noreferrer"&gt;https://github.com/takiuddinahmed/cursor-updater&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Contact
&lt;/h2&gt;

&lt;p&gt;Github: &lt;a href="https://github.com/takiuddinahmed" rel="noopener noreferrer"&gt;https://github.com/takiuddinahmed&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://takiuddin.me" rel="noopener noreferrer"&gt;https://takiuddin.me&lt;/a&gt;&lt;br&gt;
Linkedin: &lt;a href="https://www.linkedin.com/in/takiuddin-ahmed-871607b5" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/takiuddin-ahmed-871607b5&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you want Cursor updates to behave more like a package manager—without waiting for official distro support—this tool does exactly that.&lt;/p&gt;

&lt;p&gt;Install it once. Forget about updates.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>bash</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>UUIDv7 Benefits Over UUIDv4: Why and When to Use It</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Wed, 27 Aug 2025 19:44:43 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/uuidv7-benefits-over-uuidv4-why-and-when-to-use-it-1cmb</link>
      <guid>https://dev.to/takiuddinahmed/uuidv7-benefits-over-uuidv4-why-and-when-to-use-it-1cmb</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;If you’ve ever used UUIDv4 as your default unique ID, you know it’s easy and collision-resistant—but it scatters records randomly. Enter &lt;strong&gt;UUIDv7&lt;/strong&gt;, a new standard offering time‑based ordering + randomness. In this post, we’ll walk you through why UUIDv7 can be a better default going forward—and when to stick with UUIDv4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Primary keyword&lt;/strong&gt;: &lt;strong&gt;UUIDv7 benefits&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Setup
&lt;/h2&gt;

&lt;p&gt;First, let's quickly verify environment support (Python 3.12+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;span class="c"&gt;# Python 3.12 or higher required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now generate UUIDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;

&lt;span class="c1"&gt;# Generate UUIDv4 (random)
&lt;/span&gt;&lt;span class="n"&gt;uuid4_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UUIDv4:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid4_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Generate UUIDv7 (time-ordered + random)
&lt;/span&gt;&lt;span class="n"&gt;uuid7_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid7&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UUIDv7:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uuid7_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Show ordering benefit
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Generating multiple UUIDv7 values...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid7&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Expected Output
&lt;/h3&gt;

&lt;p&gt;(Your actual values will differ, but you’ll notice the &lt;code&gt;UUIDv7&lt;/code&gt; values are lexically ordered as time increases.)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UUIDv4: 3aaf40da-b143-48b4-9513-7827e95eea58
UUIDv7: 018f3de7-36d6-7b64-88fc-99d9f4d26a6d

Generating multiple UUIDv7 values...
018f3de7-36d6-7b64-88fc-99d9f4d26a6d
018f3de7-36d6-7b65-8fb4-62c4c4e7c5e9
018f3de7-36d6-7b66-8106-c8b35b9c9d3d
018f3de7-36d6-7b67-80e5-20d6c7b2a33c
018f3de7-36d6-7b68-8e1c-55882f2dbb6c
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll get two 36‑character strings. UUIDv7 includes a timestamp prefix so that new UUIDs are lexically greater than older ones. UUIDv4 is purely random.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Core Walkthrough: Why UUIDv7 Outperforms UUIDv4
&lt;/h2&gt;

&lt;h3&gt;
  
  
  2.1 Time-Sortable &amp;amp; Index-Friendly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UUIDv4 is random&lt;/strong&gt;, so indexes fragment; databases suffer more page splits, cache misses, and slower writes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UUIDv7 embeds a timestamp&lt;/strong&gt; in the most significant bits, making generated values ascending, which aids index locality and write performance.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.2 Better Cache &amp;amp; I/O Efficiency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Sequential IDs like UUIDv7 &lt;strong&gt;boost cache hits&lt;/strong&gt; and reduce disk writes in MySQL, Postgres, and even NoSQL stores.&lt;/li&gt;
&lt;li&gt;Experiments show significant reductions in I/O write volume using UUIDv7 over UUIDv4 in MySQL's InnoDB.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.3 Privacy &amp;amp; Uniqueness
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Unlike UUIDv1, UUIDv7 &lt;strong&gt;avoids embedding MAC addresses&lt;/strong&gt;, preserving privacy.&lt;/li&gt;
&lt;li&gt;Timestamp + randomness ensures global uniqueness with no coordination across systems required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2.4 Modern, Balanced Design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7 fixes known shortcomings in earlier UUID versions—removing v1’s privacy risk, v4’s sort issues, and v6’s complexity.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3. When &lt;em&gt;Not&lt;/em&gt; to Use UUIDv7
&lt;/h2&gt;

&lt;h3&gt;
  
  
  3.1 Timestamp Leakage
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Anyone with access to IDs can infer approximate creation times. This can be problematic for privacy-sensitive data.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.2 Clock Dependency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7 relies on system time. If the clock is skewed, duplicate or out‑of‑order IDs can happen. Ensure reliable clock sync or fallback logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.3 Adoption &amp;amp; Library Support
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7 is relatively new. Many languages or frameworks still lack built-in generators—may require third‑party libraries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3.4 Still Larger Than Auto-Incr
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;UUIDs (v4 or v7) take 16 bytes—more than 4–8‑byte integers. For extreme small-scale use, auto-increments may remain more efficient.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Where to Use Each
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Recommended Version&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;High-write DB (primary keys)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UUIDv7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reduces index fragmentation, improves write/read performance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs, audit trails, event streams&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UUIDv7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Chronological order naturally encoded in ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distributed systems needing order&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UUIDv7&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unique &amp;amp; sortable across nodes without coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security-critical randomness&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UUIDv4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fully unpredictable, no time leakage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy frameworks or tools&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;UUIDv4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broad support and simple fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compact, human‑friendly IDs&lt;/td&gt;
&lt;td&gt;Alternatives like ULID/K-Sortable IDs&lt;/td&gt;
&lt;td&gt;UUIDv7 is still 36 chars, not shortest or prettiest&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  5. Trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Predictability vs. Performance&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7 trades some entropy predictability for database efficiency.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Timestamp Exposure&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You expose creation time. If that violates data policies, UUIDv4 is safer.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Adoption Risk&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7’s ecosystem isn’t as mature. Be prepared to vend libraries or polyfill.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  6. FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Q: Does UUIDv7 reduce collisions more than UUIDv4?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not really—both are globally collision-resistant. UUIDv7 adds timestamp but its randomness portion still ensures uniqueness.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Q: Will every DB show big performance gains with UUIDv7?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;MySQL and write-heavy systems benefit most. PostgreSQL sees gains but they can be modest depending on indexing strategy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Q: Can I rely on millisecond resolution?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UUIDv7 spec defines millisecond granularity. Collisions are unlikely unless generating many IDs in the same millisecond.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Q: How do I handle backwards compatibility with UUIDv4?&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can transition gradually. Both types are 16-byte UUIDs, but sorting and indexing behavior will vary. Monitor performance and handle parsing correctly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Try switching a small table’s primary key to UUIDv7 and measure insert latency, index size, and query speed.&lt;/li&gt;
&lt;li&gt;Explore alternatives like ULID, KSUID, and Snowflake IDs for different trade‑offs.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;That’s your quick start guide to choosing between UUIDv7 and UUIDv4. Choose wisely and happy coding!&lt;/p&gt;

</description>
      <category>database</category>
      <category>backend</category>
      <category>programming</category>
    </item>
    <item>
      <title>What the 2025 Stack Overflow Survey Tells Us About AI Developer Tools</title>
      <dc:creator>Takiuddin Ahmed</dc:creator>
      <pubDate>Wed, 30 Jul 2025 08:34:36 +0000</pubDate>
      <link>https://dev.to/takiuddinahmed/what-the-2025-stack-overflow-survey-tells-us-about-ai-developer-tools-20ac</link>
      <guid>https://dev.to/takiuddinahmed/what-the-2025-stack-overflow-survey-tells-us-about-ai-developer-tools-20ac</guid>
      <description>&lt;p&gt;The 2025 Stack Overflow Developer Survey offers a window into how developers really feel about AI. Adoption of AI tools is booming, yet many coders remain cautious about trust, complexity and the parts of the workflow where AI is invited. This post pulls together the key points from the survey’s AI → Developer Tools section.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI tools are everywhere—but trust is low
&lt;/h2&gt;

&lt;p&gt;The survey shows that almost everyone is using or at least planning to use AI in their work. About four out of five respondents said they use or plan to use AI tools, and more than half of professional developers use AI every day. Early‑career developers (those with 1–5 years of experience) are the most enthusiastic—over half of them reach for AI tools daily.&lt;br&gt;
Despite the rapid adoption, positive feelings toward AI have cooled. This year only about 60 percent of respondents described their sentiment toward AI tools as favorable or very favorable. That’s down from over 70 percent in 2023 and 2024. Developers also don’t blindly trust AI’s output. Only about three percent of respondents said they “highly trust” the accuracy of AI tools, while nearly half expressed some level of distrust. Seasoned developers are the most skeptical; they reported the lowest rate of high trust and the highest rate of high distrust.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI handles research and documentation better than complex tasks
&lt;/h2&gt;

&lt;p&gt;Last year 35 percent of professional developers said AI struggled with complex tasks. This year that number has dropped slightly to 29 percent, but very few people believe AI truly excels at complexity. Only around four percent think AI tools handle complex coding tasks very well. Many respondents said AI is “good but not great,” and a significant portion said AI is outright bad or very poor at handling complex work. In other words, AI still isn’t ready to own mission‑critical or high‑risk tasks without human oversight.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where developers are using AI today
&lt;/h2&gt;

&lt;p&gt;When asked which parts of the workflow they use AI for, developers pointed to research, learning and documentation as the biggest areas. Here’s where AI shows up most often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Searching for answers&lt;/strong&gt; – Over half of heavy AI users rely on it to look up information and troubleshoot issues. Partial users lean on it here as well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generating content or synthetic data&lt;/strong&gt; – AI is popular for creating content like sample datasets or boilerplate text. About one‑third of heavy users and one‑third of partial users do this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning new concepts and technologies&lt;/strong&gt; – Many respondents turn to AI to understand unfamiliar languages, frameworks or techniques.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documenting code and creating documentation&lt;/strong&gt; – Around one‑third use AI to help write comments and documentation for projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understanding a codebase and debugging&lt;/strong&gt; – Some developers use AI to explore existing code or assist with debugging, although this is more common among partial users than heavy users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Writing and testing code&lt;/strong&gt; – Only a small share of heavy users say they mostly write code with AI, but a much larger share use it to write or test code partially.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the future looks like
&lt;/h2&gt;

&lt;p&gt;Developers were also asked about their plans over the next three to five years. Many said they plan to partially use AI for learning about a codebase, testing and writing code. Roughly a quarter are considering AI for predictive analytics, project planning and even deployment or monitoring. A smaller group plans to mostly use AI for generating content and documentation.&lt;br&gt;
However, there are still areas where developers want to keep AI at arm’s length. The survey notes that people show the most resistance to using AI for high‑responsibility tasks like deployment, monitoring and project planning. More than two‑thirds of respondents do not plan to use AI for project planning, and three‑quarters don’t want AI handling deployment or monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Satisfaction depends on how you use AI
&lt;/h2&gt;

&lt;p&gt;Interestingly, satisfaction with AI tools correlates with how much you use them. Those who currently use AI for most of their tasks tend to be quite satisfied. They report using AI frequently for searching and learning. People who say they plan to rely heavily on AI in the future are a little less satisfied with the tools available today, which suggests that expectations are high and current solutions haven’t fully met them yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Top frustrations
&lt;/h2&gt;

&lt;p&gt;AI isn’t perfect, and the survey highlights several pain points. The biggest frustration, cited by about two‑thirds of developers, is getting answers or code that are “almost right, but not quite.” This leads straight into the second‑biggest issue: debugging AI‑generated code takes longer than doing it yourself. Other annoyances include not using AI regularly enough to build trust, feeling less confident in one’s own problem‑solving and having trouble understanding how AI‑generated code works. Only a tiny minority said they haven’t encountered any problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  People still matter
&lt;/h2&gt;

&lt;p&gt;Even if AI becomes capable of writing and testing most of our code, developers are clear about one thing: they still want other humans in the loop. When asked about a future where AI handles most coding tasks, three‑quarters of respondents said they would still seek human help when they don’t trust AI’s answers. Many also cited ethical or security concerns, a desire to fully understand or learn best practices, being stuck on a problem, or needing to fix complex or unfamiliar code. Only about four percent imagine they won’t need help from people at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Vibe coding” is niche
&lt;/h2&gt;

&lt;p&gt;The survey also asked about “vibe coding,” which refers to generating software entirely from natural‑language prompts. The answer? Most developers aren’t doing it. About 72 percent said vibe coding is not part of their professional workflow, and only around 12 percent said they do it. A few respondents have tried it or use it somewhat, but the overwhelming majority either avoid it or dismiss it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The 2025 Stack Overflow survey paints a nuanced picture of AI in software development. On the one hand, AI tools are becoming as common as IDEs or version control: they help developers find answers, learn new things and take care of repetitive documentation tasks. On the other hand, trust and satisfaction lag behind adoption. Developers remain skeptical about the accuracy of AI output, worry about its ability to handle complex tasks and are reluctant to let AI take over high‑risk responsibilities like deployment and project planning.&lt;br&gt;
Perhaps the most important takeaway is that people still matter. No matter how sophisticated our tools get, developers plan to lean on each other for advice, sanity‑checks and ethical guidance. AI can make us faster and more efficient, but it hasn’t replaced the need for human judgment and collaboration. That’s a reassuring conclusion for anyone worried that robots are about to code us out of a job.&lt;/p&gt;

&lt;p&gt;If you enjoyed this write‑up, you can find more of my work on my personal site: &lt;a href="https://takiuddin.me" rel="noopener noreferrer"&gt;takiuddin.me&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
    </item>
  </channel>
</rss>
