MHSanaei/3x-ui · warning
a panel update is already in progress
Error message
a panel update is already in progress
What it means
startUpdate guards the whole self-update flow with a single in-process slot (acquireUpdateSlot/runID). If a previous update is genuinely still in flight (updateRunning=true), the new request gets 'a panel update is already in progress' and runID 0. The slot is released when the run finishes or on failure via the launched-flag defer — it is process-local, so a panel restart clears it.
Source
Thrown at internal/web/service/panel/panel.go:218
func (s *PanelService) GetUpdateStatus() *PanelUpdateStatus {
data, err := os.ReadFile(config.GetUpdateStatusFilePath())
if err != nil {
return &PanelUpdateStatus{State: updateStatePending}
}
var status PanelUpdateStatus
if err := json.Unmarshal(data, &status); err != nil {
return &PanelUpdateStatus{State: updateStatePending}
}
if status.State != updateStateSuccess && status.State != updateStateFailed {
status.State = updateStatePending
}
return &status
}
func (s *PanelService) startUpdate(useDev bool) (int64, error) {
runID := time.Now().UnixNano()
if !acquireUpdateSlot(runID) {
return 0, fmt.Errorf("a panel update is already in progress")
}
launched := false
defer func() {
if !launched {
releaseUpdateSlot()
}
}()
if runtime.GOOS != "linux" {
return 0, fmt.Errorf("panel web update is supported only on Linux installations")
}
bash, err := exec.LookPath("bash")
if err != nil {
return 0, fmt.Errorf("bash is required to run the panel updater: %w", err)
}
scriptPath, err := downloadPanelUpdater()View on GitHub (pinned to ad32144c42)
Solutions
- Poll the update status endpoint instead of re-issuing start until the current run reports success/failed
- If you are certain no update is running (e.g. a prior run crashed without releasing), restart the panel process to clear the slot
- Make the UI/controller disable the button while state is pending
Example fix
// before
if _, err := panelService.StartUpdate(false); err != nil { /* double fire */ }
// after
status, _ := panelService.GetUpdateStatus()
if status.State == "pending" {
// poll GetUpdateStatus until success/failed instead of starting again
} else if _, err := panelService.StartUpdate(false); err != nil { ... } Defensive patterns
Strategy: validation
Validate before calling
status, _ := panelService.GetUpdateStatus()
if status.State == "pending" {
// poll status; do not call StartUpdate while pending
} Try / catch
runID, err := panelService.StartUpdate(useDev)
if err != nil && strings.Contains(err.Error(), "already in progress") {
// switch to polling GetUpdateStatus instead of erroring to the user
} Prevention
- UI must disable the update button while status.State is pending
- Automation should start once and poll, never re-POST on timeout
When it happens
Trigger: Clicking 'update' twice, or two admins/sessions triggering the update concurrently; polling the update API in a way that re-POSTs the start action.
Common situations: Impatient double-click in the UI; an automation script that fires start instead of polling status; browser retry after a slow response.
Related errors
- panel web update is supported only on Linux installations
- bash is required to run the panel updater: %w
- failed to start panel update job: %w: %s
- failed to start panel update job: %w
- download panel updater: %w
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/23793bc28ab3543a.
Report an issue: GitHub.