当使用frpc客户端连接到frps服务端时候,这个错误会出现在客户端的日志,也会在服务端的日志中同时出现
start error: proxy [xxxxxx] already exists
frpc客户端已经没连接,却还是提示代理已经存在
frpc.toml配置中的 name = "test-tcp"名称相同的代理还在线,发起同名连接会被frps拒绝

原因是frpc客户端断开好几分钟了服务端还认为它在线,这是因为TCP层面的“假死”现象,
一般出现于客户端网络突然断开(比如拔网线、断电、NAT超时)时,客户端并没有向服务端发送正常的 TCP FIN 关闭包。此时服务端的操作系统层面认为 TCP 连接还在,
frps 应用层的心跳虽然失效了,但需要等待底层 TCP 连接真正断开才能彻底清理资源。
而操作系统的 TCP 超时时间通常很长(默认甚至能达到2小时),这就导致清理被无限期延迟。
出现这个问题,最简单的解决方案 重启frps服务端,即可正常连接
如果不重启frps,还有哪些方案可行,我们进行下来的一系列的测试和研究:
1,更换代理name,换个名字是否能解决,以tcp为例:
在already exists错误出现的时候,我们更换代理名称:比如 name = "test-tcp-111" name = "test-tcp-222"
这样虽然proxy already exists问题解决了,便会遇到新问题
wait proxy status ready error: port already used
tcp监听的端口还是在被这个代理占用中,无法连接上线,更换代理名称不行
2,耐心等待让frps自动清理,根据观察和测试发现:
Last Start: 07-12 16:53:09
Last Close: 07-12 17:10:11
差距:16:53 到 17:10 = 17分钟2秒。
大概也就15分钟上下左右,frps会检测底层 TCP 连接真正断开会彻底清理资源
等待还是让人有点着急,如何快速清理或主动让frps清理呢?
这个等待时间从何而来
它根本不是来自frps配置项 heartbeatTimeout(90秒) 或 tcpKeepalive(30秒),而是来自 Linux 内核的 TCP 重传超时(tcp_retries2=15)。
当一条 TCP 连接上有"已发送但未被 ACK 的数据"时,内核会启动重传定时器(“on” timer)。此时 SO_KEEPALIVE 完全失效,keepalive 探针根本不会发出——这是 Cloudflare 工程师实测确认的行为。
而 frps 恰恰会持续通过 yamux 连接发送应用层心跳/ping,这些数据包在客户端断网后永远收不到 ACK,于是内核进入重传循环。tcp_retries2 默认值 15,按指数退避重传,理论下限 924.6 秒,实测约 940 秒,man 手册原文描述为"约 13 到 30 分钟"。
这就解释了为什么配 tcpKeepalive=30 完全没用——因为连接上始终有 yamux 心跳数据在飞,keepalive 被"on"定时器屏蔽了。
timeline
title 17分钟清理周期分解
0s : 客户端网络异常,操作系统层面消失
~0s : frps 仍认为连接活着,继续发 yamux 心跳
~0s : 心跳数据无 ACK → 内核启动重传定时器(on timer)
~0s : SO_KEEPALIVE 被屏蔽,tcpKeepalive=30 失效
0s-940s : tcp_retries2=15 指数退避重传(约15.7分钟)
~940s : 最后一次重传失败,内核关闭TCP连接
~940s-1020s : frps 收到连接关闭事件,清理代理资源
~1020s : 40001端口被释放 (约17分钟)
搞清楚原因后,缩短清理时间的具体操作方案
1,杀掉这个代理的tcp连接
找到这个代理和客户端的tcp连接
ss -tnp | grep :7000
ESTAB 0 0 [::ffff:10.6.0.11]:7000 [::ffff:184.123.132.124]:2962 users:(("frps",pid=1974774,fd=12))
精准杀掉该客户端在 7000 端口上的连接
找到客户端 IP 后,执行:
# 把 <客户端IP> 替换为你在上面看到的IP
ss -K state established sport = :7000 dst <客户端IP>
ss -K state established sport = :7000 dst 184.123.132.124
执行完这个命令后,frps 会瞬间收到 TCP RST 重置包,立刻判定客户端掉线,并自动清理掉掉线代理端口的监听。此时你再启动 frpc,就不会再报 proxy already exists 了,麻烦的是你需要知道客户端ip精准设置
2,降低 tcp_retries2,这是唯一直接作用于这17分钟的旋钮。tcp_retries2 与超时时间的大致对应关系:
tcp_retries2 超时下限 说明
15 (默认) 924s (15.4min) 观察到的17分钟左右
8 100s RFC 1122 推荐最小值
5 25-30s 适合内网穿透场景
3 7-10s 极激进,公网抖动可能误杀
# 临时设置,先观察(建议从8开始)
sysctl -w net.ipv4.tcp_retries2=8
# 验证当前值
sysctl net.ipv4.tcp_retries2
# 如果没问题,永久写入
echo "net.ipv4.tcp_retries2 = 8" >> /etc/sysctl.conf
sysctl -p
回滚方式(随时可恢复):
sysctl -w net.ipv4.tcp_retries2=15
# 删除 /etc/sysctl.conf 中新增的行
在frps所在服务器系统这样配置=8可以将等待时间缩短到大概3分钟就会自动清理
3,TCP_USER_TIMEOUT(理论最优,需改源码)
TCP_USER_TIMEOUT 是唯一能覆盖重传定时器的 socket 选项——设了之后,无论重传到第几次,只要超过设定时间没收到任何 ACK,立刻 abort 连接。但 Go 标准库没直接暴露,frp 也没用,需要二次开发时通过 syscall 设置。
你若做二次开发最值得加的一行代码。