I'm searching for a option to remove all unused images every minute.

The images I use are for Craft Commerce.


Best way to handle this is probably with a console command provided by a module.

The console command would need to fetch all asset IDs that don’t have any corresponding rows in the relations database table.

use Craft;
use craft\db\Query;
use craft\db\Table;
use craft\elements\Asset;
use yii\console\ExitCode;
use craft\helpers\Console;

public function actionPurgeUnusedAssets(): int
    // Find any asset IDs that aren't related to anything
    $assetIds = (new Query())
        ->from(['a' => Table::ASSETS])
        ->leftJoin(Table::RELATIONS . ' r', '[[r.targetId]] = [[a.id]]')
        ->where(['r.id' => null])

    if (empty($assetIds)) {
        $this->stdout('No unrelated assets to delete.' . PHP_EOL);
        return ExitCode::OK;

    // Now fetch and delete them
    $assets = Asset::find()

    $this->stdout('Found ' . count($assets) . ' unrelated assets.' . PHP_EOL);

    foreach ($assets as $asset) {
        $this->stdout(" - Deleting asset {$asset->filename} ... ");
        $this->stdout('done' . PHP_EOL, Console::FG_GREEN);

    $this->stdout('Finished deleting unrelated assets.' . PHP_EOL);
    return ExitCode::OK;

Then, you can set up a cron job on your server that hits this command every 24 hours.

0 0 * * * /path/to/project/craft your-module/purge-unused-assets
  • 0 0 * * * /path/to/project/craft your-module/action-purge-unused-assets is it not this or? – Bram Beekman Feb 7 at 14:47
  • @BramBeekman No, you don't include the action- part in the command. – Brandon Kelly Feb 7 at 15:02
  • $this->stdout(" - Deleting asset {$$asset->filename} ... "); -> $this->stdout(" - Deleting asset {$asset->filename} ... "); – Bram Beekman Feb 8 at 8:00
  • @BramBeekman Thanks, fixed. – Brandon Kelly Feb 8 at 13:36

I once wrote a custom service doing this (not for commerce, though). I'll post the code, and maybe it can help you along the way. You can set it up in a module, make a console controller, and have a cronjob hit it every minute (although, are you sure you need to do it that often?).

The DeleteAssets is a very simple extension of BaseJob, basically looping through the assets, and deleting them one by one, just so it's handled as queue job. You could also loop through and delete each with Craft::$app->getElements()->deleteElementById(), but doing it as a queue job is probably better.

    public function deleteUnusedAssets() {

        //First fetch all ids from elements table
        $allIds = Entry::find()

        // Query for all related Assets (assets that are used somewhere)
        $usedAssets = Asset::find()

        // Query for all assets NOT in the list of the used ones
        $excludeString = 'and, not ' . implode(', not ', $usedAssets);
        $unUsedAssets = Asset::find()

        $queue = Craft::$app->getQueue();
        $jobID = $queue->push(new DeleteAssets([
            'description' => 'Deleting ' . count($unUsedAssets) . ' assets'),
            'elements' => $unUsedAssets,

  • Unable to locate message source for category 'cinema-program'. – Bram Beekman Feb 6 at 14:25
  • Ah, that is the handle for my plugin. Just use your own, or replace description with something that doesn't use the translate function. I'll update the answer. – KSP Feb 9 at 19:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.