VeltrixDB / Resources / Migration guide
Migration playbook · 12 min read

Moving from DynamoDB to VeltrixDB in two sprints.

A staged playbook: shadow reads, dual-writes, cutover checklist, and the rollback plan you'll hopefully never need — with a Terraform starting point. Coming from Redis instead? See the Redis migration section.

AG
Akanksha Gupta
Founder · VeltrixDB
Published · May 7, 2026 Read time · 12 minutes For · SREs & backend tech leads
01
Why teams migrate

Two reasons, always the same.

!

Update (Sept 2026): parts of this playbook referred to tooling that does not exist, and those steps have been corrected. There is no ddb-importer image and no DynamoDB Streams sidecar. The bulk-load step now converts the DynamoDB export to JSONL and loads it with the repo's veltrix-migrate import. The cost (3–5×) and latency (12 ms → 4.9 ms) comparisons have no measured source and have been removed. The 4.9 ms figure comes from an internal, unverified run. VeltrixDB is self-hosted only; there is no managed service today. It speaks its own binary and text protocols, not RESP or the DynamoDB API, so your application has to use a VeltrixDB client. Claims about earlier customer migrations (counts, durations, zero rollbacks) could not be sourced and have been removed (Oct 2026).

Every team that walked us through a DynamoDB-to-VeltrixDB migration showed up with the same two concerns. The bill, and the tail.

If you're reading this guide, you probably already know your why. For the engineering manager you'll be selling this migration to, make the case with your own numbers:

  • Cost · take your current DynamoDB invoice and compare it with the cost of self-hosted nodes with local NVMe sized for your RAM and disk needs. VeltrixDB keeps every key in RAM. With small values the index (~142 B/key, plus ~90 B/key for the ordered index unless disabled) is about as large as the data, so size RAM from your key count. The node-sizing rules give an estimate, not a measurement.
  • Tail latency · measure DynamoDB's P99 for your access pattern, then measure VeltrixDB's during the shadow-read phase (§06). The only reproducible VeltrixDB read figure we publish is a cache-resident YCSB run (P99 2.77 ms). Its conditions are in the benchmark post.
Planned duration
2 sprints
a plan, not a measured median
P99 latency drop
—
measure it in shadow reads
Cost reduction
—
no measured figure · use your invoice

The rest of this guide is the playbook. Two sprints. No big-bang cutover. Rollback at every stage.

02
Pre-flight

Confirm you're in scope — before you book the project.

VeltrixDB is a scalpel, not a Swiss Army knife. Spend an hour validating fit before you spend two sprints validating performance.

Before you start, run the table through this checklist. If any row is a "no," talk to us before booking the migration — there's a good chance VeltrixDB is the wrong tool for that workload.

Workload fit
  • Your hot path is point lookups — GET / PUT / DEL / HGET-style. Ordered range scans exist (RANGE, SCANCUR), but in cluster modes they read only the local node (and on v1.13 and earlier skip the Raft read barrier; Fixed in v1.14: they pass it). So a critical path built on Query with sort-key ranges, or on Scan, needs a careful look.
  • GSI access patterns either become separate keys, or a secondary index on a record field (IDXCREATE name field / IDXQUERY name value). The field is read from a JSON or k=v value, so store items as flat JSON (unmarshal DynamoDB JSON first). IDXQUERY fans out to every node in a cluster; an index is equality-only.
  • You have a partition key in DynamoDB that can map 1:1 to a VeltrixDB key. Composite (PK + SK) keys map to pk:sk string concatenations.
  • Values are ≤ 400 KB (DynamoDB's own limit). VeltrixDB supports up to 64 MB, but smaller values keep your RAM budget honest.
  • You can run on bare-metal NVMe or local SSD. That is what the engine is designed and benchmarked for. Network-attached block storage (EBS, Azure managed disks) will run, but every fdatasync is then a network round trip, and nothing has been measured there.
!

If you're using DynamoDB Streams · we don't ship a Streams consumer. The dual-write phase (§05) keeps VeltrixDB current without one. If you'd rather mirror from Streams, write a small consumer (for example a Lambda) that applies each change with a VeltrixDB client. On the VeltrixDB side, CDC is available at /admin/cdc, and repl-ship forwards it to another VeltrixDB cluster.

03
The plan

Two sprints. Five stages.

No big-bang cutover. Every stage runs DynamoDB in parallel until the very last hour. Rollback is one config flip away at every checkpoint.

Migration timeline · 2 sprints / 14 working days
Days 1–2 Sprint 1 · Stage 1
Stand up the cluster
Deploy in your own VPC (self-hosted) with the Helm chart or operator described in the VeltrixDB README. Wire up Prometheus + Grafana to the metrics endpoint on :2112. Validate health checks against an empty cluster. No production traffic yet.
Days 3–5 Sprint 1 · Stage 2
Bulk-load the snapshot
Use DynamoDB's ExportTableToPointInTime to S3, convert the export to JSONL, and pipe it into veltrix-migrate import. We have no measured import rate, so time a sample before you plan the window. Validate row count and a sample of values match (veltrix-migrate export dumps the same JSONL back).
Days 6–9 Sprint 1 · Stage 3
Enable dual-writes
Route all writes to both DynamoDB and VeltrixDB synchronously. Failure handling: log mismatches, alert on divergence > 0.1%, but do not roll back individual writes. DynamoDB remains the source of truth.
Days 10–11 Sprint 2 · Stage 4
Shadow reads
For each read served by DynamoDB, fire an async read against VeltrixDB. Compare responses, log mismatches, surface P99 latency on a side-by-side Grafana dashboard. Run for a full 48-hour weekend cycle.
Days 12–13 Sprint 2 · Stage 5
Gradual cutover
Flip the read source one cohort at a time — 5%, 25%, 50%, 100%. Each step requires the previous step's P99 to be stable for 4 hours. Rollback is a feature-flag flip, not a deploy.
Day 14 Wrap-up
Decommission DynamoDB writes
Drop the dual-write fan-out from your app. Optionally keep the DynamoDB table read-only for 7 days as cold backup. Save your final invoice — it's the receipt you bring to the QBR.
04
Infrastructure

Terraform you can paste into your repo.

Three resources, eight inputs. Drop into your existing EKS module, change the instance type to a local-NVMe SKU, run terraform apply.

This snippet provisions a 3-node EKS-hosted VeltrixDB cluster on i3en.6xlarge instances with local NVMe. Adjust node count and instance class, or swap the EKS parts for GKE/AKS modules. The Helm chart is not in the main VeltrixDB repo. Check the chart repository listed in the VeltrixDB README and pin a chart version that exists there. Your values.yaml.tpl has to use that chart's own value names.

terraform · veltrixdb.tf
# Helm release wrapping the VeltrixDB chart
resource "helm_release" "veltrixdb" {
  name       = "veltrixdb"
  repository = "https://charts.veltrixdb.io"
  chart      = "veltrixdb"
  # version = "…"  pin a chart version published in that repository
  namespace  = "veltrixdb"
  create_namespace = true

  values = [templatefile("./values.yaml.tpl", {
    nodes          = 3
    cache_mb       = 131072  # server flag -cache is in MB (default 256)
    nvme_gb        = 3000
    storage_class  = "veltrixdb-nvme"
    region         = "us-east-1"
  })]
}

# Local NVMe StorageClass (i3en family)
resource "kubernetes_storage_class" "nvme" {
  metadata { name = "veltrixdb-nvme" }
  storage_provisioner = "kubernetes.io/no-provisioner"
  volume_binding_mode = "WaitForFirstConsumer"
  parameters = {
    type = "local-ssd"
    fsType = "xfs"  # nvme-prep formats local SSDs as XFS
  }
}

# Node group: i3en.6xlarge — 24 vCPU, 192 GiB RAM, 2 × 7.5 TB local NVMe
module "veltrix_nodes" {
  source           = "terraform-aws-modules/eks/aws//modules/eks-managed-node-group"
  cluster_name     = var.eks_cluster_name
  name             = "veltrixdb-nodes"
  instance_types   = ["i3en.6xlarge"]
  desired_size     = 3
  min_size         = 3
  max_size         = 6
  labels = { workload = "veltrixdb" }
  taints = [{ key = "workload", value = "veltrixdb", effect = "NO_SCHEDULE" }]
}

Bulk-import from DynamoDB

There is no dedicated DynamoDB importer. The repo's veltrix-migrate import reads JSONL from stdin, one {"k": "<key>", "v": "<base64 value>"} per line, and PUTs each entry over the binary protocol. It's idempotent: re-runs overwrite, so it's safe to restart if it fails partway. The jq step below maps each DynamoDB item to a key (here, a partition key pk of string type) and stores the item's JSON as the value. Adapt it to your key schema, for example pk:sk for composite keys. It sends one PUT at a time and does not carry TTLs. Each single PUT takes a whole 4 KB VLog block, so for many small items a loader that batches with MPUT (e.g. the Go client's BinaryConn.MPut) packs ~26 × 128 B records per block instead. In --mode=raft, point --dst at the current leader, because followers reject writes with a MOVED redirect and veltrix-migrate does not follow it. In --mode=replicated any node accepts the write and replicates it to its peers (eventual by default).

~/migration · zsh
$ aws dynamodb export-table-to-point-in-time \
    --table-arn arn:aws:dynamodb:us-east-1:123456789012:table/sessions \
    --s3-bucket veltrix-migration-staging \
    --export-format DYNAMODB_JSON

$ go build -o veltrix-migrate ./cmd/veltrix-migrate   # from the VeltrixDB repo

$ aws s3 cp --recursive \
    s3://veltrix-migration-staging/AWSDynamoDB/<export-id>/data/ ./export/

$ gunzip -c ./export/*.json.gz \
  | jq -c '{k: .Item.pk.S, v: (.Item | tojson | @base64)}' \
  | ./veltrix-migrate import --dst veltrixdb-0.veltrixdb:9000
05
Dual writes

Fan out writes, keep DynamoDB as the source of truth.

During sprint 1, every write goes to both databases synchronously. If divergence climbs above 0.1%, an alarm fires and you fix it before you ever flip reads.

The dual-write step is where most botched migrations go wrong. The temptation is to fan out writes asynchronously to keep latency down — don't. Async fan-out hides bugs that only surface during cutover. Run the writes synchronously, in parallel, and accept the latency cost during the migration window.

Reference implementation

The pattern, in pseudocode, looks like this. The key invariants: write to DynamoDB first (still source of truth), then write to VeltrixDB, then log any inconsistencies — but never roll back a successful DynamoDB write because the VeltrixDB write failed.

repo/sessions/dual_writer.go · r.veltrix is a *client.Client (github.com/VeltrixDB/veltrixdb/client)
func (r *Repo) Put(ctx context.Context, k string, v []byte) error {
    // 1. Source of truth still wins
    if err := r.ddb.Put(ctx, k, v); err != nil {
        return err
    }
    // 2. Mirror to VeltrixDB · best-effort
    go func() {
        cctx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
        defer cancel()
        if err := r.veltrix.Put(cctx, k, v); err != nil {
            metrics.DualWriteFail.WithLabelValues("veltrix").Inc()
            log.Warn("veltrix mirror failed", "key", k, "err", err)
        }
    }()
    return nil
}
i

Watch the divergence metric. A healthy dual-write phase should see <0.01% mirror failures, almost entirely network blips. If you see >0.1%, something is wrong — most often an encoder that doesn't round-trip a value byte-for-byte (VeltrixDB accepts values up to 64 MB, well above DynamoDB's 400 KB item limit). Fix before proceeding.

06
Shadow reads

Read both. Trust neither. Compare everything.

Before flipping the read path, you spend 48 hours reading from both databases and comparing the answers byte-for-byte. This is where you catch the bugs that bulk-import didn't.

The shadow-read phase runs on the same dual-write pattern, inverted. For every read your application makes, fire a second read against VeltrixDB, compare, log mismatches, and surface the side-by-side P99 chart in Grafana. Your users still see DynamoDB's answer — VeltrixDB is on probation.

Shadow-read exit criteria
  • Mismatch rate < 0.001% over a full weekend cycle (catches any time-zone or weekend-batch effects)
  • VeltrixDB P99 read latency < 8ms for 4 hours straight under peak prod load
  • Cache hit rate > 90% in steady state — the LIRS cache should be warm by now
  • Zero errors from the VeltrixDB client during the window (VeltrixDB speaks TCP, not HTTP, so there are no 5xx codes to count)
  • An incident runbook merged that documents the rollback procedure for the next stage
07
Cutover

5 → 25 → 50 → 100. No big-bang flips.

Move the read traffic in four feature-flag controlled steps. Each step must be stable for four hours before the next. Rollback at any step is a single config change.

If shadow reads were clean this stage should be uneventful — walk through the cohorts anyway. Discipline here is what makes the postmortem boring.

Cutover cohorts · 16-hour walk
0h–4h
5% of read traffic → VeltrixDB
Flag read_source=veltrix applied to a 5% user-id hash range. Watch P99, error rate, customer-support tickets. No deploys during this window.
4h–8h
25%
Same hash-range scheme, expanded. If the 5% window showed no anomalies, the 25% should be uneventful. Halt if you see anything you don't recognise.
8h–12h
50%
Half your traffic now reads from VeltrixDB. This is the step where any cold-cache effects would surface in production — they shouldn't, because shadow reads warmed the cache for 48 hours, but watch the hit rate anyway.
12h–16h
100%
All traffic on VeltrixDB. DynamoDB still receiving writes (you haven't decommissioned the dual-write yet — that's the next step). Save the Grafana snapshot. Send it to your engineering manager.

Rollback

If anything goes wrong at any step, the rollback is a single feature-flag flip: read_source=dynamodb. Because writes are still going to both databases, DynamoDB never went stale. You lose nothing, and you debug at leisure.

✓

Clean shadow reads are what make this step boring. Typical problems — a serialization quirk, a Unicode normalization edge case — surface during the 48-hour shadow window, where they cost nothing to fix. That's the entire point of running shadow-reads.

08
Wrap-up

Decommission, save the receipts.

A week after cutover, drop the dual-write fan-out and put the DynamoDB table into read-only mode. Keep it warm for a month as cold storage. Then archive it.

The decommission step is procedural — no surprises. Drop the dual_writer.go path from your repo, point the DynamoDB IAM role at a read-only policy, and schedule a calendar reminder for 30 days out to delete the table. Most teams keep the table around longer than they need to — that's fine, and the cost is trivial.

What's not trivial: save the DynamoDB invoice from the month before cutover. Compare it to the VeltrixDB invoice three months later. Remember that VeltrixDB's side of that comparison is your own infrastructure bill, since it is self-hosted. The delta is the receipt you bring to the next budget review, and the number your CFO will want to see in writing. (An earlier version quoted customer savings figures that we cannot source, so we have removed them.)

Commands checked against the VeltrixDB repo (cmd/veltrix-migrate, client/) · Oct 2026 · durations are a plan, not measurements.

Continue reading

Want a solutions architect on the migration?

Engineering support (terms agreed per engagement) covers migration planning from DynamoDB or Redis, a sizing review on your workload, and incident help during cutover.