Immich vs PhotoPrism: the library model decides which one you keep
The short version: PhotoPrism indexes a folder you already own and leaves it alone. Immich takes a copy, renames it to a UUID and keeps the meaning in Postgres. Both handled the same 205 files; the difference shows up the day you try to leave.
Pick Immich if the library is mostly phone photos from several people and you want the app to be the product. Pick PhotoPrism if you already have a photo folder with a structure you trust and you want software that reads it instead of absorbing it.
That is the whole decision. Everything below is what I measured to be sure.
Two different ideas about what a photo is
PhotoPrism treats the filesystem as the record. originals is a directory tree you can open in any file manager, and the index is a cache built by reading it. Delete the index and you lose albums, labels and edits; the photos are exactly where they were, with the names you gave them.
Immich treats the database as the record. An upload produces an asset row in Postgres, and the file is stored under a path derived from that row. The filename you uploaded is metadata, not a location.
Neither is wrong. They fail differently, and one of those failures is much easier to live with.
The test: the same 205 files, twice
I generated 205 JPEGs with Pillow 13.0.0 and set EXIF dates across 2025: 200 unique images cycling through 12 MP, 6 MP and 2 MP, plus five byte-identical copies under different names. Total 139,139,243 bytes. No real photographs, and that matters later.
Then the same directory went into both products on the same box: 4 vCPU, 7.75 GiB of RAM, 46 GB of root disk, Docker 29.8.0, no GPU. PhotoPrism was photoprism/photoprism:latest, reported as 261029-963b58c50-Linux-AMD64-Plus. Immich was ghcr.io/immich-app/immich-server:v3.3.6, with the vector-enabled Postgres image and a valkey container. I ran them one at a time and removed each stack before starting the next, because disk was the real constraint.
Immich’s quickstart compose did not come up first try. Plain postgres:14-alpine is refused at migration with ‘No vector extension found’, the server exits 1, and it does not retry a database that is not listening yet: the first boot logged connect ECONNREFUSED 172.18.0.3:5432 and killed the API process. The fix is the project’s own Postgres image plus IMMICH_VECTOR_EXTENSION=vectorchord. Worth knowing before you spend an hour on your compose file.
Where the files actually go
PhotoPrism read /photoprism/originals in place. After indexing, the directory was byte-identical: same 139,139,243 bytes, mtimes untouched at 20:07:04 to 20:07:14. Everything it built went into a separate storage directory: a 1,085,440-byte SQLite database, 1,600 preview files, five small YAML sidecars.
Immich copied each upload into this shape, with the storage template off, which is the default:
/data/upload/295566af-2a2a-4587-b786-fbeff521141a/56/dd/56dd40a6-e1da-4091-b1f1-ba30e0af47b0.jpg
That is user id, two levels of two hex characters taken from the asset uuid, then the uuid itself. There is no IMG_0189.jpg anywhere. The name exists in one column of one table. Fine until you want to find a photo without the app, at which point it is not fine at all.
Immich can make those paths human-readable with a storage template — {{y}}/{{MM}}/{{filename}} and so on — and it can migrate an existing library onto a new template. The default does not, and most people never change it. PhotoPrism’s equivalent question never arises, because it never owned the path in the first place.
What the previews cost
Both products generate previews. Both generate more than you expect.
PhotoPrism made eight thumbnail files per photo: 1,600 files, 103,563,996 bytes. That is 76.3% of the 135.7 MB the 200 indexed images occupy, or 74.4% of the 139.1 MB sitting in the folder, because the five duplicate files are on disk and not in the index. Each took 111 to 252 ms according to the log, and the full index of 205 files took 45.8 seconds, measured three ways: the log line (index: updated 205 files [45.59866694s]), the wall clock around the command (46,586 ms), and the last thumbnail’s own timer.
Immich made two derived files per asset, a thumbnail and a preview, and did it asynchronously. Uploading the same 205 files took 10,707 ms, which is 52 ms per file over HTTP, returning 200 for the 200 new assets and a duplicate response for the five copies. That number is not comparable to PhotoPrism’s 45.8 seconds, because Immich had not finished: the queue was still working. A second batch of 25 fresh 6 MP files gave the honest figure: 1,218 ms to upload, 3,926 ms until both derived files per asset were on disk. About 157 ms per photo, end to end.
Immich’s previews for the 205-file run came to 64,040,401 bytes, 47.2% of the originals. Across 275 assets, including the sharper 6 MP batch, the ratio rose to 63.0%. Two files instead of eight, and still more than half the library again in previews. If you are sizing a disk, size it for originals plus half.
The numbers side by side
| PhotoPrism 261029 | Immich v3.3.6 | |
|---|---|---|
| Files after the run | 205 originals, in place | 205 originals, copied and renamed |
| Photo records | 200 | 200 |
| Index database | 1.08 MB SQLite | 145 MB Postgres (275 assets, geodata included) |
| Preview files | 1,600, eight per photo | 400 for 205 assets, two per asset |
| Preview size | 103.6 MB, 76.3% of 135.7 MB | 64.0 MB, 47.2% of 135.7 MB |
| Time for 205 files | 45.8 s full index | 10.7 s upload, processing after |
| Memory while serving | 623 MiB, one container | 1.78 to 2.03 GiB server, 442 MiB Postgres, 5 MiB Redis |
The 145 MB database is not 275 photos’ worth of rows. Most of it is the reverse-geocoding data Immich loads on first boot; the log counted past 190,000 geodata records before it settled. PhotoPrism’s 1.08 MB is a directory index with labels and hashes. I have one measurement point on each, so I am not going to draw curves from them.
Duplicates expose the model
Five of the 205 files were byte-identical copies. Both products noticed, and responded in opposite ways.
PhotoPrism produced 200 photo records from 205 files. Its files table holds 195 IMG_* rows and 5 DUP_* rows, no photo has two file rows, and sum(file_size) comes to 135,722,340 bytes — the 200 unique images to within 13 bytes. The duplicate did not become a second photo, and it did not remain visible as a second location either. PhotoPrism also wrote five YAML sidecars for the duplicate names, 53,732 bytes in total.
Immich took 200 assets and answered HTTP 200 rather than 201 for the five duplicates, creating nothing. Same outcome, different mechanism: a uniqueness constraint on the checksum rather than a stacking model. 275 assets and 275 originals on disk, which is the invariant worth holding on to.
If you have ever wondered where the disk went after a bad import, that invariant is the first thing to check. A library showing 300,000 files and 240,000 assets is telling you something.
The migration question nobody asks before installing
PhotoPrism’s exit is cp -a. The originals are yours, named as you named them, in the folder structure you chose. You lose albums and labels, which live in the index and the sidecars. You keep every photograph.
Immich’s exit takes more work. Dumping Postgres gives you the meaning, which is albums, people, dates and descriptions, and the files come out as UUIDs unless you turned the storage template on. There is an export path, and it is not a folder.
This is not an argument against Immich. It is an argument for turning the storage template on the day you install it, before the library is large enough that the migration job takes a weekend. It is two minutes of configuration at 300 photos and considerably more at 300,000.
The same asymmetry shapes backup design, which I covered for Immich specifically: the photos are the easy half and the database is the library. PhotoPrism inverts that: back up the folder and you have the photos; back up the index and you have the photos plus the sorting. Either way the rule from the backup stack I run holds: a folder of JPEGs on its own is not a photo library backup.
Which one I would run
For a household with three phones, shared albums, and people who will not read a manual: Immich. The app is the product, the multi-user model is real, and the storage template fixes the only thing that would otherwise bite you later.
For a box whose photo folder predates the software, or a collection organised by year and event that you navigate in a file manager as often as in a browser: PhotoPrism. It reads the folder you have instead of replacing it, uses 623 MiB to Immich’s 2.2 GiB on this box, and adds a tenth of the database.
For a box with 3.4 GB free on its root filesystem: neither, until you have sorted the disk out. Pulling the 2.51 GB PhotoPrism image took this machine to 820 MB free and 99% used, the same afternoon-ruining condition I wrote about for a full docker vdisk, arriving from a different direction.
What this test did not cover
No faces, because the images are noise and I ran Immich with machine learning disabled to keep the footprint down. Face detection, facial recognition and smart search are three of Immich’s strongest features, and this bench says nothing about any of them.
No video, RAW, HEIC or Live Photos. No library large enough to make either database work for a living. No phone upload, which is how almost everyone actually gets photos in. No restore drill, which is the only test that matters for a library you cannot re-download.
The honest summary is narrower than the headline: PhotoPrism keeps your files where they are, Immich keeps your library in a database and stores files by id, and preview overhead came out at 76% against 47% of the originals on this set. Those three facts should decide more of your choice than any feature grid.