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

  1. Poll the update status endpoint instead of re-issuing start until the current run reports success/failed
  2. If you are certain no update is running (e.g. a prior run crashed without releasing), restart the panel process to clear the slot
  3. 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

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


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/23793bc28ab3543a. Report an issue: GitHub.