Azure Files Deep Dive: Lessons from a 15TB Production Migration
Eight QNAP shares, 15TB, eleven million files, six weeks, four incidents, one Saturday cutover. This is what actually happened — the RoboCopy flags that made the difference, the SID remap that nearly broke the timeline, the bandwidth surprise that extended the upload phase, and the DFS-N redirect that made cutover invisible to users.
The Environment, the Brief, and What We Walked Into
The client was a 280-person professional services firm running eight SMB file shares off a QNAP TS-h2490FU NAS — a 24-bay enterprise unit nearing end of lease. The brief was clear: migrate all eight shares to Azure Files before the lease expires, maintain access continuity, preserve all permissions, and keep the Finance and HR shares inaccessible to anyone who should not have them. The NAS was not domain-joined. The client was on a 500 Mbps internet line with a QoS policy that throttled upload bandwidth to 60% during business hours. Nobody had told us about the QoS policy when we agreed the timeline. The timeline was wrong by eleven days.
This article is a field account of that migration — what we planned, what we found when we started, what broke, what we did about it, and the specific technical steps that got us from 15TB on a QNAP to 15TB on Azure Files with a two-hour Saturday cutover window that users never noticed. Every number, every error, and every lesson in this article is from the actual migration logs.
Six Weeks, Start to Finish — What Actually Happened
The plan said four weeks. The reality was six. Here is the week-by-week account of what happened, what surprised us, and what we changed mid-stream.
Discovery, inventory, and the first surprise
Deployed Azure Migrate appliance for file share discovery. Ran the PowerShell compatibility scan. Found 23 files with illegal characters — all date-stamped filenames using colons. Found one path at 2,096 characters. Fixed both before any data moved. Ran the bandwidth measurement: 500 Mbps line, 300 Mbps available at business hours. Calculated daily transfer capacity: 414 GB/day. Estimated raw transfer time: 36 days. Applied 1.2× AFS overhead: 43 days. The original timeline of four weeks was built on theoretical line capacity, not measured throughput. Timeline reset: six weeks.
Infrastructure provisioning, Private Endpoints, identity configuration
Provisioned three storage accounts via Terraform. Created Private Endpoints for each (file sub-resource). Configured private DNS zone and linked to corporate VNet. Disabled public access after verifying Test-NetConnection returned private IPs. Ran AzFilesHybrid to domain-join all three storage accounts to on-prem AD DS. Assigned share-level RBAC roles. Set root directory ACLs on each target share before any data copy — this saved hours during the upload phase. Discovered the QNAP was not domain-joined, meaning local QNAP SIDs needed remapping. SID remap timeline calculated: 177 hours across active shares with 50% buffer. Allocated Week 3 entirely to SID remap.
SID remap — the week nobody planned for
The QNAP used local user accounts mapped to Active Directory through a proprietary LDAP bridge. Every file ACL referenced a QNAP-local SID that Azure Files could not resolve. The remap required mounting each share on a domain-joined Windows Server intermediate, running icacls to replace local SIDs with AD SIDs, and then uploading to Azure Files. The remap ran overnight for seven nights. Finance alone took 89 hours (59 raw + 50% buffer). On night four, the intermediate Windows Server ran out of disk space — it was a 500GB VM provisioned for a different purpose. Added a 2TB data disk and resumed. Total remap duration: 177 hours spread over 8 days, running in parallel where volume independence allowed.
Initial RoboCopy runs over 10GbE LAN — Wave 1 archives complete
Archives and Backups migrated first via Azure Storage Mover (one-time, no hybrid sync needed). Initial RoboCopy of active shares ran over 10GbE LAN from intermediate Windows Server to Azure Files: 8–20 threads, /COPY:DATSO /DCOPY:DAT /B /MIR /IT flags. First RoboCopy run for Finance took 62 hours — 3.2TB at 8 threads over 10GbE. Switched to 20 threads for subsequent shares: Projects transferred in 41 hours. Archive shares completed via Storage Mover in parallel: 3.8TB in 28 hours. Wave 1 signed off: archives, media, backups complete. Go/no-go passed.
Delta syncs, Wave 2 validation, QoS incident
Started delta RoboCopy runs (second pass with /MIR — only changes since first run). Wave 2 (Projects, Shared) delta runs completed in 4 hours each. Tested access with real user accounts from Windows 10, Windows 11, and macOS. macOS required SMB over HTTPS configuration — port 445 was blocked by the client's ISP for home-based users connecting via P2S VPN. Identified and fixed. Week 5 incident: IT team pushed a QoS firmware update to the Palo Alto firewall that reclassified SMB traffic and throttled it to 100 Mbps. Detected by Azure Monitor latency alerts. Fixed within 3 hours. Lesson: always have storage-specific monitoring alerts live before migration traffic starts.
Final delta syncs, Wave 3 cutover, post-migration validation
Saturday 22:00: final RoboCopy run for Finance and HR (delta only — minutes, not hours). Set QNAP to read-only. Ran one final RoboCopy pass to catch last-minute changes. Executed DFS-N target change for all eight shares. Verified access with Finance team member on standby. Zero errors. Monday morning: 0 help desk calls in the first two hours. 2 calls by 10am — both from users confused by the different drive letter appearance in File Explorer (cosmetic, not functional). QNAP retained in read-only for 14 days post-cutover. Decommissioned after 14-day validation period with no issues.
The Four Incidents — What Broke and How We Fixed It
Four distinct incidents occurred during this migration. All four were avoidable with better pre-migration discovery. All four are documented here in full — not to list them as cautionary tales, but because the exact root cause and fix are the most useful parts of a field account.
The Exact RoboCopy Flags That Worked — and Why
RoboCopy has dozens of flags. The combination below is what we used for this migration, tuned through three iterations. Each flag has a specific reason. Use this as a reference, but understand what each flag does before applying it to your environment — some flags interact in non-obvious ways when combined with Azure File Sync cloud tiering.
Initial bulk copy — Run 1 (users still on QNAP)
The first run copies everything. Users are still working on the QNAP during this run — files will change. That is expected and handled by subsequent delta runs. Do not block user access during the initial run. Run from the domain-joined intermediate Windows Server with the QNAP mounted as the source and the Azure Files share mounted as the target.
RoboCopy S:\ T:\ ^
/COPY:DATSO ^ # Copy Data, Attributes, Timestamps, Security (NTFS ACLs), and Ownership
/DCOPY:DAT ^ # Copy directory Data, Attributes, Timestamps
/B ^ # Backup mode — bypasses file access restrictions, reads locked files
/MIR ^ # Mirror — deletes on target what no longer exists on source
/IT ^ # Include Tweaked files (same date/size but different attributes)
/R:3 /W:10 ^ # 3 retries, 10 second wait — balanced for transient Azure network errors
/MT:20 ^ # 20 threads — optimal for initial Azure Files upload (8–20 recommended by Microsoft)
/LOG+:C:\Logs\robocopy-finance-run1.log ^ # Append log — never overwrite
/TEE ^ # Output to console AND log file simultaneously
/NP ^ # No Progress — suppresses per-file % to reduce log size
/XD "$RECYCLE.BIN" "System Volume Information" ^ # Exclude system directories
/XF "desktop.ini" "thumbs.db" # Exclude Windows metadata files not needed in Azure Files
RoboCopy S:\ T:\ ^
/COPY:DATSO /DCOPY:DAT /B /MIR /IT ^
/R:3 /W:10 ^
/MT:16 ^ # Reduce threads for delta runs — processor-bound rather than bandwidth-bound
/LOG+:C:\Logs\robocopy-finance-delta.log ^
/TEE /NP ^
/XD "$RECYCLE.BIN" "System Volume Information" ^
/XF "desktop.ini" "thumbs.db"
The SID Remap — What It Is, Why It Matters, and the Exact Procedure
This was the week that nobody had planned for and that nearly broke the timeline. The QNAP used local user accounts mapped to Active Directory via LDAP. Every file's ACL contained a QNAP-local SID — an identifier that exists only within the QNAP's local user database. Azure Files, which resolves identities against Active Directory and Entra ID, cannot interpret these SIDs. If you copy files with local SIDs directly to Azure Files, the files arrive with permissions that reference identities Azure Files cannot resolve. Users get Access Denied even when they should have access.
The remap must happen on a domain-joined Windows Server intermediate: mount the QNAP share, run icacls to replace local SIDs with their corresponding AD SIDs using a mapping table, then copy the remapped share to Azure Files. The formula for estimating remap duration: (file count × unique ACL entries × 0.0001) ÷ 2,000 entries/minute = hours. Run in parallel where shares are independent to reduce wall-clock time.
"S-1-5-21-QNAP-1001" = "S-1-5-21-DOMAIN-500001" # QNAP\johndoe → DOMAIN\johndoe
"S-1-5-21-QNAP-1002" = "S-1-5-21-DOMAIN-500002" # QNAP\janedoe → DOMAIN\janedoe
"S-1-5-21-QNAP-1050" = "S-1-5-21-DOMAIN-500050" # QNAP\svcaccount → DOMAIN\svcaccount
}
# Step 2: Remap ACLs on each file — run on mounted QNAP share Get-ChildItem -Path "S:\Finance" -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
$acl = Get-Acl -Path $_.FullName -ErrorAction SilentlyContinue
if ($acl) {
$modified = $false
foreach ($ace in $acl.Access) {
$oldSid = $ace.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
if ($sidMap.ContainsKey($oldSid)) {
$newIdentity = New-Object System.Security.Principal.SecurityIdentifier($sidMap[$oldSid])
# Remove old ACE, add new ACE with AD SID
$acl.RemoveAccessRule($ace)
$newRule = New-Object System.Security.AccessControl.FileSystemAccessRule($newIdentity, $ace.FileSystemRights, $ace.InheritanceFlags, $ace.PropagationFlags, $ace.AccessControlType)
$acl.AddAccessRule($newRule)
$modified = $true
}
}
if ($modified) { Set-Acl -Path $_.FullName -AclObject $acl -ErrorAction SilentlyContinue }
}
}
# After remap: run RoboCopy with /COPY:DATSO to copy remapped ACLs to Azure Files
The Saturday Night Cutover — Step by Step
The cutover window was 22:00 Saturday to 06:00 Sunday. We used two hours. Here is the exact sequence, with the commands that ran at each step.
Verify all delta syncs are complete and Azure Files shares are healthy
Ran final delta RoboCopy for all active shares. Finance: 22 minutes. HR: 8 minutes. Projects: 31 minutes. Verified file counts match between QNAP and Azure Files for each share using Get-ChildItem recursive count. Checked Azure Monitor: no throttling alerts, storage account health green. Confirmed Finance team member and IT lead were on standby. Help desk briefed and on call.
Freeze the source — no further writes to QNAP
Set all eight QNAP shares to read-only via QNAP admin panel (Shared Folders → [share] → Edit → Permissions → Read only for all users). This freezes the dataset. No new files can be written. Ran one final RoboCopy pass for Finance and HR (the two highest-churn shares): Finance delta completed in 4 minutes, HR in 2 minutes. The data gap at cutover was zero minutes of uncopied changes.
Redirect all share paths to Azure Files with a single PowerShell sequence
Ran the DFS-N target change for all eight shares in sequence. Each command took under three seconds to execute.
$namespace = "\\corp.local\files"
$storageAccount = "corpazfilespr.file.core.windows.net" # Premium SSD account
$storageAccountStd = "corpazfilesstd.file.core.windows.net" # Standard HDD account
# Disable old QNAP targets (do not delete — keep for rollback) Set-DfsnFolderTarget -Path "$namespace\Finance" -TargetPath "\\QNAP\Finance" -State Offline
Set-DfsnFolderTarget -Path "$namespace\HR" -TargetPath "\\QNAP\HR" -State Offline
# ... repeat for all 8 shares
# Add Azure Files as new active targets New-DfsnFolderTarget -Path "$namespace\Finance" -TargetPath "\\$storageAccount\finance" -State Online
New-DfsnFolderTarget -Path "$namespace\HR" -TargetPath "\\$storageAccount\hr" -State Online
New-DfsnFolderTarget -Path "$namespace\Projects" -TargetPath "\\$storageAccountStd\projects" -State Online
New-DfsnFolderTarget -Path "$namespace\Shared" -TargetPath "\\$storageAccountStd\shared" -State Online
# ... repeat for remaining 4 shares
# Verify: all targets should show Azure Files as active, QNAP as offline Get-DfsnFolderTarget -Path "$namespace\Finance" | Select TargetPath, State
Verify with real user accounts from real client machines
Finance team member (on standby) opened an Excel workbook from \\corp.local\files\Finance\Reports\Q2-2026.xlsx. File opened in 3 seconds — within normal range. Created a test file, deleted it, confirmed timestamps appeared correctly. HR share verified identically. Checked Azure Monitor: successful auth events appearing in sign-in logs within 30 seconds of DFS-N change. Confirmed Private DNS resolution was returning correct private IPs from three test clients in different network segments.
Document completion and hand over to monitoring
All eight shares verified. Azure Monitor dashboards showed normal throughput and auth patterns. QNAP retained in read-only mode for 14-day rollback window. Sent cutover completion notification to stakeholders at 22:42. Help desk briefed on Monday morning watch list: any user who cannot access a mapped drive should call immediately. Help desk calls on Monday: 2 (both cosmetic UI questions, resolved in 5 minutes each). Decommissioning of QNAP scheduled for Day 15 post-cutover.
What We Would Do Differently
These are the seven things we would change if running this migration again — not general best practices, but specific decisions that cost us time or created incidents in this project.
- Provision the intermediate server with a dedicated 2TB+ data disk from day one. Incident 1 cost six hours because we added the disk on Day 4. The disk cost $12/month. The rework cost half a day of engineer time. This is the easiest call in retrospect.
- Run the SID remap estimation calculation before agreeing the project timeline. The SID remap added eight days we had not accounted for. The formula is known — (files × unique ACLs × 0.0001) ÷ 2,000 entries/min = hours — but nobody ran it until we were already in Week 2. Run it in Week 1 during the inventory phase.
- Test port 445 from every network type during pre-migration validation, not just from the office. Incident 2 (macOS users blocked) was discovered in Week 5 because we tested from domain-joined office machines only. Home users on P2S VPN were not tested until three days before cutover. Test every OS type from every network segment — office, home VPN, cellular hotspot — in Week 1.
- Test Kerberos ticket renewal for all applications before cutover. Incident 4 (application access failures after 10 hours) is the kind of failure that a production migration stress test would have caught. Simulate a 12-hour connected session with each application that holds persistent file handles and verify it reconnects correctly at the 10-hour Kerberos expiry mark.
- Place the network firewall QoS firmware update on the change freeze list. Incident 3 happened because a security patch was classified as non-impacting. For active migration windows, add all network hardware changes to the change advisory process regardless of classification. The firewall vendor's "patch" reclassified Azure Files traffic at a firmware level.
- Set root directory ACLs before any data is copied — not after. We set root ACLs correctly, but only after discovering in another engagement that setting them after a large dataset has been written causes ACL propagation to take hours. Document this in the procedure and make it a gate, not an afterthought.
- Run /COPY:DATSO not /COPYALL. We caught this on Run 1 of the Finance share when the RoboCopy log was filled with "Auditing ACL" copy errors. Switched to /COPY:DATSO for Run 2. The log became clean and readable. An unreadable log during a production migration is a real risk — you cannot identify genuine errors through the noise.
"Every incident in this migration was discoverable before migration started. None of them required new knowledge. All of them required running the specific check — the bandwidth measurement with QoS applied, the SID remap estimation, the port 445 test from a home connection, the 10-hour Kerberos session test. The planning phase is not overhead. It is the migration."
— Francis Avorgbedor | Azure Field Notes