The System Behind the Timeline
Immich’s architecture answers a practical question: how can mobile uploads, web browsing, command-line imports, slow media processing, machine-learning features, and durable archive state belong to one product without all competing in the same request? This chapter follows the boundaries between those responsibilities. The goal is not to tour containers, but to understand what crosses each interface, who owns the resulting state, and why the separation helps in some situations while adding operational work in others.
The answer begins with a stable API. It then branches into work that should finish quickly for a user and work that can wait in a queue. The branches meet again in durable records and media files, while machine learning remains an optional, resource-sensitive dependency.
One API, three kinds of client
Immich has three main clients: a mobile app, a responsive web app, and a command-line utility intended especially for bulk upload. They do not need to be identical because they do not serve the same immediate purpose. The phone is close to the camera and must manage local albums, connectivity, and background scheduling. The web app is a browser-based way to browse, administer, edit, and share. The CLI is suited to moving an existing archive or automating uploads. What they share is the server-facing contract.
The architecture documentation says all three use REST clients generated from an OpenAPI description. In plain terms, OpenAPI describes the available requests and responses in a form that tools can use to generate client code. That shared contract reduces the need for each client to invent its own interpretation of assets, users, albums, or permissions. It does not make the clients interchangeable. A phone can have local persistence and operating-system limits that a browser does not; a CLI can process a bulk archive without presenting a visual timeline.
A request such as “show this album” follows the interactive path. The client sends an HTTP request to the server API. The server authenticates the request, applies the relevant application rules, reads the state needed for the response, and returns data or a media stream. The client then presents the result in its own way. The important ownership boundary is that the client owns the interaction, while the server owns the shared interpretation of the archive.
That arrangement also protects the archive from being defined by one interface. A photo uploaded from the CLI can later appear in the web interface. An asset backed up from a phone can be viewed by another authorized client. The common API is what makes those actions refer to one server-side model rather than three disconnected collections.
The boundary is not free of complexity. An API can be available while a client is unable to complete a background upload because the mobile operating system has postponed the task. A web request can return an asset before every derived preview or search index has been built. A common contract gives clients a shared vocabulary; it does not eliminate the different conditions under which they operate.
Why interactive work must separate from media work
The server receives two broad kinds of demand. The first is interactive: return a timeline, save an album change, authenticate a user, or deliver a preview. The second is processing: extract metadata, generate thumbnails, transcode video, create a contextual search index, detect faces, or run a scheduled maintenance task. These workloads have different timing and resource profiles.
If a large import required every thumbnail and video derivative to finish inside the upload request, the user would be forced to wait for work whose duration depends on the number, format, and size of the files. If a model-intensive operation ran directly in the same path as a timeline request, a burst of indexing could compete with ordinary browsing. Immich therefore uses background jobs. The API accepts or records the event, and a worker later performs the heavier operation.
Redis, used through BullMQ, coordinates those jobs. A queue is not the same thing as the archive database. The queue answers questions such as which work is waiting, which worker should pick it up, and which job should follow another. Postgres answers a different class of question: which users, assets, albums, paths, permissions, and processing results are part of the durable system state. Redis helps move work through the system; Postgres preserves the application’s account of what that work means.
This distinction clarifies why a job queue can be busy without implying that the original asset is missing. A newly uploaded item can already have an identity and a stored original while thumbnail generation is waiting. Conversely, a worker can finish creating a derivative while a database record still needs to capture the result. Immich’s documented job order expresses these dependencies: metadata extraction comes before storage-template migration when enabled, thumbnail generation precedes smart search, face detection, OCR, and video transcoding, and face detection precedes facial recognition. The exact internal queue behavior can vary by version, but the dependency idea is fundamental.
The order also explains why one slow stage can affect several visible features at once. Thumbnail generation is not just a cosmetic optimization in the documented workflow. It provides the previews used by later work, so a backlog there can delay contextual search, face detection, OCR, and video processing even when the API remains responsive. Face recognition has a similar dependency: detecting a face and producing the information needed to compare it are separate from assigning faces to a person. A reader who sees an asset but not a person cluster is seeing an incomplete processing state, not necessarily a different asset.
Immich exposes job status and queue controls to administrators because this distinction needs to be observable. The operator can see that a feature is waiting for work rather than infer from an empty search result that the media is absent. Some work is also scheduled rather than triggered by a single upload; memories and other maintenance tasks can run on a timetable. That means “everything uploaded” and “the archive is fully indexed” are different milestones. The documentation does not establish one universal completion time, so a responsible expectation is based on workload and queue state, not on a fixed promise.
This model gives workers a clear success condition: complete the assigned transformation and record enough result state for the next dependent job or user action. It also gives the operator a useful failure question. If an asset is present but lacks a preview, inspect processing work. If previews exist but contextual results do not, inspect the later indexing stage. If a job cannot reach the media or database, changing the client will not solve the worker’s boundary problem. The architecture makes those questions possible even though the sources do not specify one complete retry or failure policy for every job.
The server’s worker arrangement needs a careful reading because the word “microservices” can describe a responsibility as well as a container. The architecture documentation describes an immich-server container for REST requests and scheduling and an immich-microservices container that primarily consumes jobs. The worker documentation also describes API and microservices workers inside the server and explains how an operator can copy the service and configure one instance for API work and another for background work. The current checked-in Compose definition presents a service named immich-server alongside the machine-learning, Redis, and database services rather than requiring one fixed topology.
These descriptions are best understood as two deployment choices around the same boundary, not as a promise that every release uses the same container names. The logical distinction is API work versus queued work. An operator can keep them together for a simpler installation or split and throttle them when heavy processing should not compete with interactive traffic. Splitting may improve control over contention, but it also means more services, more configuration, and more places where a mount or environment setting must be kept consistent.
The diagram shows the system as a set of responsibilities, not as a claim that every arrow is a direct network call in every configuration. The worker is the important bridge. It consumes queued work, uses the filesystem for media operations, consults Postgres for durable state, and calls the machine-learning service when a job needs inference. That bridge is also where many operational assumptions meet: the worker needs access to the same relevant media, the queue must be reachable, and the database must be healthy enough to record progress.
This is why a deployment that looks correct from the browser can still be incomplete. The API may be able to list an external or uploaded asset while the worker that generates its derivatives cannot reach the corresponding media location. A split-worker arrangement therefore changes more than CPU allocation. It changes which process must see which state and which configuration must stay aligned. The worker documentation presents splitting as an option for throttling or distributing work, not as a requirement for every installation. The simpler arrangement reduces coordination overhead; the separated arrangement gives an operator more control when interactive traffic and batch processing compete.
The same boundary helps explain why the API should not be judged solely by whether it returns quickly. A fast response can mean that the request only recorded an event and placed work in the queue. A later response may include more complete information because workers have added metadata or derivatives. In a self-hosted system, the useful operational question is therefore two-part: did the server accept the request, and did the responsible worker reach the state needed for the user’s next action? Those are related outcomes, but they are not the same one.
Machine learning is separate because its conditions are different
Immich isolates machine-learning operations in a Python service rather than making every server process carry model execution. The architecture documentation gives two practical reasons. Python is a natural environment for ML work, and the service has specific hardware and memory requirements. Running it separately makes it possible to disable the service, place it on another machine, or choose an acceleration path without redesigning the entire API layer.
The separation changes ownership of the work, not the user’s mental model of the result. A search or face job begins as an Immich background task. The worker sends the relevant metadata or image-derived input to the ML service. The service checks whether the required model is available, loads and configures it, and can cache loaded models for reuse. The result returns to the Immich system, where it becomes part of searchable or clustered state. The original media remains the archive’s source; the model output is an additional representation.
That design creates flexibility and cost at the same time. The current ML project documents CPU and several accelerator-oriented options, and the Compose setup gives the ML service its own model cache. But models can be large, and the requirements documentation calls for at least 6 GB of RAM for a smooth experience, with more recommended; it also records platform and processor constraints for the current generation. An operator who moves ML to another machine trades local simplicity for network configuration and another dependency. An operator who disables it avoids model resource costs but gives up contextual search and facial features.
The same principle applies to Postgres. The database is not an incidental sidecar whose location can be chosen solely by convenience. Immich’s requirements documentation says stable database performance matters and recommends local SSD storage rather than a network share. The recommendation reflects the role of Postgres as durable, frequently accessed state. An architecture can separate components logically and still depend on physical placement for acceptable behavior.
The published hardware guidance is a starting point, not a capacity guarantee. Immich documents a minimum of 6 GB of RAM and 2 CPU cores, with more recommended, and notes that machine-learning models can be memory-intensive. A small collection with ML disabled and a large collection being indexed are different workloads even if they run the same containers. Likewise, moving ML to a separate host may free the main host from model execution while introducing network reachability and another service to monitor. The architecture creates options; it does not remove the need to match those options to the archive and its workload.
What the boundaries protect—and what they expose
The architecture succeeds when each boundary has a clear owner. Clients handle interaction and local conditions. The API provides a shared contract. Queues absorb work that can finish later. Workers turn jobs into files and recorded results. Postgres preserves relationships and permissions. The filesystem holds media content. Machine learning adds derived representations under its own hardware and model constraints.
The boundaries also expose failure modes that a single-process mental model would miss. A client may not send a file when the operating system delays background work. A queue may accumulate work faster than workers can process it. A worker may lack the mount needed to reach an external library even though the API container can see it. A model may consume more memory than the host can provide. A database may be reachable but placed on storage that makes the whole application feel unstable. The architecture does not make these conditions disappear; it makes them separable enough to diagnose and change.
That is the main system-level trade-off. Separation prevents one kind of workload from automatically becoming every other kind of workload, and it lets operators place sensitive or expensive capabilities where they fit. But every separated responsibility creates an interface that must be configured, observed, and recovered. The more an operator customizes worker placement, external mounts, accelerators, or remote ML, the more important it becomes to know which boundary owns each piece of state.
With that map in place, the next useful test is temporal rather than structural. Follow one photo from the moment it is selected for backup through checksum handling, queueing, metadata extraction, thumbnail generation, and the later work that makes it searchable and playable. That sequence shows how the boundaries behave when an asset is arriving, rather than merely naming what the components are.