Bulk delete assets (images and videos) from Immich
TL;DR: I messed up while importing a big library into immich and need to roll this back somehow.
I’ve a semi large immich setup (5tb) with 2 users. I mistakenly uploaded around 500GB of assets into other user’s account instead of my own account due to my messup of the API keys through immich-go.
I was wondering if there is a clean approach to fix the ownership issue instead of deleting everything and starting over. Apparently not
I thought about doing something like this but I was not sure if the physical files can stay under /upload/user2_id while owner is user1_id and what that would mean for other sub components for immich:
UPDATE "asset" SET "ownerId" = 'user1\_id' WHERE "ownerId" = 'user2\_id' AND "createdAt" >= '2025-08-26';
Apparently, it’s somewhat possible but not everything will work without additional work in the db and possibly moving physical files according to the reddit Immich community, huge thanks!
Then I had to go for safer path by deleting the assets from the other account then reupload. Surprise, this is not easy either. Most of the info online is outdated, I don’t see “Recently uploaded” button anymore in the UI; Date based search is not giving the exact images I uploaded. So what can i do?
Let’s pull the asset ids from the db into a file since i know how to do it:
docker exec immich_postgres psql -U $DB_USERNAME -d $DB_NAME -c "SELECT * FROM assets WHERE \"ownerId\" = '$OWNER_ID' AND \"createdAt\" >= '2025-08-26';" > asset_ids_to_be_deleted.txt
Then I need some python power to read this file and call immich asset deletion endpoint through the api. ofc you can do that from curl too
import requests | |
from datetime import datetime | |
# === CONFIG === | |
IMMICH_URL = "http://localhost:2283/api" | |
API_KEY = os.environ.get("IMMICH_API_KEY") | |
DATE_AFTER = "2025-08-25T00:00:00.000Z" | |
DATE_BEFORE = "2025-08-29T00:00:00.000Z" | |
HEADERS = { | |
"x-api-key": API_KEY, | |
"Accept": "application/json", | |
"Content-Type": "application/json", | |
} | |
# First two lines are from postgres output, useless. | |
# Also the lines have single space as prefix, need to eliminate that too. | |
to_delete = [x.strip() for x in open('asset_ids_to_be_deleted.txt', 'r').read().splitlines()[2:]] | |
print('to_delete', to_delete[:10]) | |
print(f"Found {len(to_delete)} assets to delete.") | |
if not to_delete: | |
exit("Nothing to delete. Check your date range.") | |
# === STEP 3: Delete in batches (API limit: 100 per call) === | |
# I can only make 1 work, any larger number was not deleting fully. | |
#BATCH_SIZE = 100 | |
BATCH_SIZE = 1 | |
for i in range(0, len(to_delete), BATCH_SIZE): | |
batch = to_delete[i:i+BATCH_SIZE] | |
print(f"Deleting batch {i//BATCH_SIZE+1} ({len(batch)} items)...") | |
# print('batch', batch) | |
del_resp = requests.delete( | |
f"{IMMICH_URL}/assets", | |
headers=HEADERS, | |
json={"ids": batch, "force": True} | |
) | |
print('del_resp', del_resp) | |
if del_resp.status_code == 200: | |
print("Batch deleted successfully.") | |
else: | |
print("Error:", del_resp.status_code, del_resp.text) | |
print("✅ Deletion complete!") |
Tricky part is setting the batch size to 1; I tried with multiple numbers, nothing worked even though api response is 200, the assets are not deleted.
SELECT count(*) FROM assets WHERE "ownerId" = '$OWNER_ID' AND "createdAt" >= '2025-08-26';