Slot 42 WHM troubleshooting
WHM Transfer Tool Stuck at 80%: Every Cause and Fix
The WHM Transfer Tool sits at 80% for hours and nobody tells you why. Here is what that stage actually does, how to read the session logs, and how to fix it.
The progress bar in WHM’s Transfer Tool has been at 80% for three hours and the screen has not changed. Nothing has errored, nothing has finished, and your maintenance window is closing. The Transfer Tool is almost never the broken part. One sub-step is slow, or one sub-step is blocked by something outside the transfer entirely.
What the tool is actually doing at that stage
By the time the bar creeps into the high seventies, the cheap work is done. The Transfer Tool has connected to the source over root SSH, enumerated accounts, copied packages and feature lists, and created the accounts on the destination. What remains is the restore phase: home directory contents, MySQL databases, mail, and the per-account metadata it replays at the end.
That phase is where the bytes are. A 400 GB account and a 400 MB account both take one slot in the progress calculation, but one of them takes four minutes and the other takes eleven hours.
“80%” is an estimate, not a measurement
I have watched people kill a healthy eight-hour transfer over this number. WHM derives the percentage from how many queued items the session has finished, weighted by phase, not from bytes moved. When it parks at 80%, the queue has one or a few long-running items still open, and the tool has no way to say “this rsync is 3% through a 400 GB maildir”.
The stall has a small set of causes. Work through them in this order.
Read the session logs before you touch anything
Every transfer and restore session writes to its own subdirectory under /var/:
ls -lt /var/cpanel/transfer_sessions/
cd /var/cpanel/transfer_sessions/<SESSION_ID>/
ls -lS
Inside you get master.log, a startup log, and per-item logs whose filenames contain the module and the username. The master log is the timeline of what was dispatched and when. The per-item log tells you what that item is doing right now. They are line-delimited JSON rather than plain prose, so each line is one event with its own timestamp, and tail -f still reads fine once your eye stops expecting sentences.
The single most useful command on a stuck session:
ls -lt /var/cpanel/transfer_sessions/<SESSION_ID>/ | head
tail -f /var/cpanel/transfer_sessions/<SESSION_ID>/<newest-file>
If you would rather not go digging on disk, WHM API 1 hands you the same file. Both arguments are required:
whmapi1 fetch_transfer_session_log transfer_session_id=<SESSION_ID> logfile=master.log
If the newest file has a recent modification time and is still growing, the transfer is alive and slow. If nothing in that directory has been touched for twenty minutes, something is blocked. Those are two different problems. Do not guess which one you have.
Cause 1: SSH
WHM’s Transfer Tool requires root SSH access to the source server. There is no way around that; without root on the source, the tool is not an option and you fall back to per-account cpmove archives. That dependency is also the most common stall.
Verify the exact connection the tool uses, from the destination:
ssh -p 22 -i /root/.ssh/id_rsa -o BatchMode=yes root@SOURCE_IP 'id; uptime'
If that hangs or asks for a password, fix it before restarting the session. Non-standard port, key not in the source’s /root/, wrong permissions (700 on .ssh, 600 on the key), or PermitRootLogin set to prohibit-password when you are passing a password all produce the same “it just sits there” behaviour.
The subtler failure is a session timeout killing a transfer that was working. A five-hour rsync over an idle-looking control channel gets reaped by an intermediate firewall or by sshd itself. Set keepalives on both ends.
On the destination, in /root/.ssh/config:
Host *
ServerAliveInterval 30
ServerAliveCountMax 120
TCPKeepAlive yes
On the source, in /etc/:
ClientAliveInterval 60
ClientAliveCountMax 60
TCPKeepAlive yes
Then systemctl reload sshd on the source. I set these before every large transfer now rather than after the first failure.
Cause 2: The destination ran out of disk or inodes
Free space is the obvious check. Inodes catch people out, because a filesystem with 200 GB free and zero free inodes stalls silently instead of erroring cleanly.
df -h
df -i
Maildir accounts are the usual source of inode exhaustion: one file per message, multiplied across years of retained mail. If df -i shows IUse% at 100%, find the offender:
du --inodes -d 1 /home | sort -n | tail -20
Extending the filesystem is the real fix. Deleting a few log files buys you minutes.
Cause 3: One enormous account, mailbox, or database
Find out what is actually running rather than assuming:
ps auxf | grep -E 'rsync|pkgacct|restorepkg|cpmove|mysql|tar' | grep -v grep
ps auxf gives you the process tree, so you can see which child of the transfer process is live. Then follow the file handles:
lsof -p <PID> | grep -E 'home|mysql'
That tells you the exact path being written. If it is one 90 GB mailbox or one 60 GB database, the answer is patience or a change of plan, not a restart. Restarting throws away the hours already spent.
Cause 4: MySQL and MariaDB stalls
Database restore is where long stalls become hours. Four things cause it:
- A version mismatch. Moving from MariaDB to MySQL, or downgrading a version, means dumps that import slowly or partially. Check both ends with
mysql -Vbefore you start. - One huge InnoDB table. A single 40 GB table imports as one long insert stream and builds its indexes as it goes. Nothing reports progress.
- The schema upgrade pass afterwards. cPanel runs it over the restored databases at the end of each account, and on a server with thousands of tables that alone takes a long time.
- Locking. Another process holding a lock will park the import indefinitely.
Look at the server, not the log:
mysqladmin processlist
mysql -e "SHOW FULL PROCESSLIST\G"
tail -50 /var/lib/mysql/$(hostname).err
If the import is genuinely running, you can speed the remainder up on the destination temporarily:
# /etc/my.cnf (revert after the migration)
innodb_flush_log_at_trx_commit = 2
innodb_buffer_pool_size = 4G
Restart MySQL only between accounts, never mid-import, and put those values back afterwards.
Cause 5: cpsrvd restarted or something got OOM-killed
If the log directory went quiet with no error, check whether the process was killed underneath the session:
dmesg -T | grep -iE 'killed process|out of memory'
grep -iE 'oom|cpsrvd|restart' /var/log/messages | tail -40
tail -100 /usr/local/cpanel/logs/error_log
A transfer of large accounts on a small-RAM destination will absolutely trip the OOM killer, usually during the database import. If you find an OOM kill, add swap or reduce concurrency before you retry; the same run will fail the same way.
Cause 6: CSF or LFD banned the other end mid-transfer
This one is deceptive because the transfer starts fine. A long migration opens many connections, LFD’s flood detection reads that as an attack, and the source IP gets banned an hour in. Everything freezes with no error.
csf -g SOURCE_IP
grep SOURCE_IP /var/log/lfd.log
Whitelist both directions before starting, on both servers:
csf -a SOURCE_IP "migration source"
echo "SOURCE_IP" >> /etc/csf/csf.ignore
csf -r && systemctl restart lfd
csf.allow stops the firewall dropping traffic; csf.ignore stops LFD deciding to ban it in the first place. You need both.
Aborting and resuming safely
Do not kill the process from the shell. Abort the session properly so WHM writes the queue state out:
whmapi1 abort_transfer_session transfer_session_id=<SESSION_ID>
This is what the Abort button in WHM calls. Give it a minute to unwind, confirm no transfer children remain in ps auxf, then check what actually landed. Accounts that completed are real accounts on the destination; they do not need redoing. Start a fresh session containing only the accounts that did not finish, and enable the overwrite option only for accounts you know are incomplete.
When to stop and use cpmove instead
If a single account is the problem, take it out of the batch and move it by hand. This is faster and far easier to observe:
# on the source
/scripts/pkgacct --skiphomedir username
rsync -aHz --progress /home/cpmove-username.tar.gz root@DEST_IP:/home/
# on the destination
/scripts/restorepkg /home/cpmove-username.tar.gz
# then sync the home directory separately, resumable
rsync -aHz --partial --progress /home/username/ root@DEST_IP:/home/username/
Splitting the archive from the home directory is the trick. The archive restore recreates the account structure quickly, and the rsync is resumable, so a dropped connection costs you nothing. The same manual approach is covered end to end in migrating cPanel to cPanel without WHM.
Pre-flight checklist
| Check | Command | What you want |
|---|---|---|
| Root SSH from destination to source | ssh -o BatchMode=yes root@SOURCE 'id' |
Returns instantly as uid=0 |
| Free space | df -h |
Destination free space > source used, plus 20% |
| Free inodes | df -i |
Well under 80% used |
| MySQL versions | mysql -V on both |
Same major family, destination not older |
| Firewall | csf -g SOURCE_IP |
Allowed and ignored on both ends |
| SSH keepalives | /etc/ |
ClientAliveInterval set on the source |
| Largest accounts | du -sh / |
Move anything oversized separately |
Ten minutes on that table saves the three hours of staring at a frozen bar.
FAQ
How long is too long at 80%?
Judge by log activity, not by the clock. If the newest file in the session directory is still growing, let it run, even overnight. If nothing has changed in twenty minutes and no relevant process appears in ps auxf, it is stuck and waiting longer will not help.
Does aborting lose the accounts already transferred?
No. Accounts that finished are fully restored on the destination. Aborting only stops the remaining queue. Re-running a completed account with overwrite enabled is what loses data, so exclude those from the retry.
Why does the same account fail every time?
Almost always disk, inodes, memory, or one pathological database. Move that account with pkgacct and restorepkg on its own and watch the output live; the failure names itself within a couple of minutes.
Where this fits
Most stuck transfers are diagnosable in ten minutes once you stop reading the progress bar and start reading the session logs. If you would rather hand the whole window to someone who has done it thousands of times, that is what my server migration work covers, including the accounts that the Transfer Tool refuses to move cleanly.