A Real-World Concurrency Bug Caused by LibreOffice Profile Locking
Back to Blog

A Real-World Concurrency Bug Caused by LibreOffice Profile Locking

Concurrency issues are always annoying to debug. Things work perfectly most of the time, then suddenly fail only under specific timing conditions, often without obvious errors. I recently ran into one of those issues in a Go backend service where some document conversion jobs randomly failed whenever multiple jobs ran at the same time.

After tracing through the conversion pipeline, the actual culprit turned out to be LibreOffice running in headless mode.

The Problem

The conversion logic itself was fairly straightforward:

cmd := exec.Command(
    "/usr/bin/soffice",
    "--headless",
    "--convert-to",
    "csv",
    "report.xlsx",
)

cmd.Run()

At first glance, this looks completely safe to run concurrently. Each request launches its own process, so it is easy to assume the executions are isolated from each other.

The problem is that LibreOffice internally uses a shared user profile directory, usually something like:

~/.config/libreoffice/

When multiple soffice processes run simultaneously under the same Linux user, LibreOffice attempts to lock that shared profile. The first process acquires the lock successfully, while the second process may fail to initialize properly.

Depending on how the application handles errors, this can lead to a variety of confusing symptoms:

  • missing output files
  • empty generated files
  • silent conversion failures
  • intermittent production-only bugs

In my case, concurrent jobs occasionally produced empty output even though the process itself appeared to complete successfully.

Why This Is Easy to Miss

This issue can stay hidden for a surprisingly long time. Local development environments usually have very low concurrency, and many systems unintentionally serialize jobs without realizing it. As a result, LibreOffice rarely ends up competing with itself during testing.

The issue often only appears later, once concurrency increases in production. A change as simple as adding more workers or allowing multiple jobs to run simultaneously can suddenly make the bug reproducible.

The Fix

LibreOffice provides a built-in way to isolate profiles per process using:

-env:UserInstallation=

Instead of letting every process share the default profile directory, each invocation can use its own temporary profile. In Go, the fix looked like this:

profileDir := fmt.Sprintf(
    "/tmp/libreoffice-profile-%d",
    time.Now().UnixNano(),
)

defer os.RemoveAll(profileDir)

cmd := exec.Command(
    "/usr/bin/soffice",
    fmt.Sprintf(
        "-env:UserInstallation=file://%s",
        profileDir,
    ),
    "--headless",
    "--convert-to",
    "csv",
    "report.xlsx",
)

cmd.Run()

The important part is the -env:UserInstallation= argument. By assigning a unique profile directory to every invocation, each LibreOffice process becomes isolated from the others and no longer competes for the same lock.