containerd/containerd · warning
container is already in starting state
Error message
container is already in starting state
What it means
The CRI start path sets `Starting = true` inside an atomic status update; if a previous StartContainer on the same container already set that flag and is still running, a second start is rejected to prevent two concurrent starts of one container.
Source
Thrown at internal/cri/server/container_start.go:224
)
return &runtime.StartContainerResponse{}, nil
}
// setContainerStarting sets the container into starting state. In starting state, the
// container will not be removed or started again.
func setContainerStarting(container containerstore.Container) error {
return container.Status.Update(func(status containerstore.Status) (containerstore.Status, error) {
// Return error if container is not in created state.
if status.State() != runtime.ContainerState_CONTAINER_CREATED {
return status, fmt.Errorf("container is in %s state", criContainerStateToString(status.State()))
}
// Do not start the container when there is a removal in progress.
if status.Removing {
return status, errors.New("container is in removing state, can't be started")
}
if status.Starting {
return status, errors.New("container is already in starting state")
}
status.Starting = true
return status, nil
})
}
// resetContainerStarting resets the container starting state on start failure. So
// that we could remove the container later.
func resetContainerStarting(container containerstore.Container) error {
return container.Status.Update(func(status containerstore.Status) (containerstore.Status, error) {
status.Starting = false
return status, nil
})
}
// createContainerLoggers creates container loggers and return write closer for stdout and stderr.
func (c *criService) createContainerLoggers(logPath string, tty bool) (stdout io.WriteCloser, stderr io.WriteCloser, err error) {
if logPath != "" {View on GitHub (pinned to 4246446a2b)
Solutions
- Wait for the first start to complete (poll ContainerStatus for RUNNING) instead of retrying immediately.
- Deduplicate start requests per container ID in the caller.
- Increase client timeout so slow starts are not retried while in flight.
Example fix
// before
client.StartContainer(ctx, id) // retry while first start still running
// after
err := client.StartContainer(ctx, id)
if err != nil && strings.Contains(err.Error(), "already in starting state") {
return nil // first start still in progress; wait instead
} Defensive patterns
Strategy: try-catch
Type guard
func isAlreadyStarting(err error) bool { return err != nil && strings.Contains(err.Error(), "already in starting state") } Try / catch
if err := client.StartContainer(ctx, id); err != nil {
if isAlreadyStarting(err) {
return nil // first start in progress; poll for RUNNING instead
}
return err
} Prevention
- Single-flight start requests per container ID
- Use generous timeouts for start to avoid premature retries
- Poll ContainerStatus for RUNNING instead of re-issuing start
When it happens
Trigger: Two concurrent StartContainer calls for the same container ID; a retried start after a client timeout while the first start is still in flight inside the status update closure in internal/cri/server/container_start.go.
Common situations: kubelet retry loops issuing duplicate starts; a CRI client with aggressive timeouts retrying a slow start; duplicate events from a controller reconciler.
Related errors
- container is in starting state, can't be removed
- container is already in removing state
- container is in removing state, can't be started
- container is in %s state
- unable to get sandbox %q runtime info: %w
AI-assisted analysis of containerd/containerd@4246446a2b (2026-09-02).
Data as JSON: /api/errors/7bb1177b0b264991.
Report an issue: GitHub.