How Immich Generates Personalized "On This Day" Memories in `memory.service.ts`

Immich creates personalized "On This Day" memories through a daily background job that queries each user's assets by day-of-year, groups them by original year, and generates time-boxed memory records visible only to the asset owner.

Immich's Memories feature surfaces nostalgic photo collections from years past, automatically appearing in users' timelines without manual curation. This functionality is orchestrated by memory.service.ts in the immich-app/immich repository, which implements a sophisticated background job pipeline that ensures each user sees only their own historical content.

The Background Job Architecture

The memory generation process is triggered by the MemoryGenerate job, registered via the @OnJob decorator in server/src/services/memory.service.ts. This attaches the onMemoriesCreate() method to the background task queue, ensuring execution occurs automatically on schedule.

@OnJob({ name: JobName.MemoryGenerate, queue: QueueName.BackgroundTask })
async onMemoriesCreate() {
  // Implementation handles per-user memory generation
}

When the scheduler fires this daily job, the service iterates through all active users to create personalized batches.

The Memory Generation Pipeline

The core logic in memory.service.ts follows a stateful, idempotent workflow that prevents duplicate processing while handling failures gracefully.

User Enumeration and Isolation

The service fetches all active users via this.userRepository.getList({ withDeleted: false }). This enumeration ensures memories are generated individually for each user, establishing the foundation for personalization through the subsequent ownerId parameter passing.

State Management and Deduplication

To prevent concurrent execution conflicts, the service acquires DatabaseLock.MemoryCreation. It tracks progress in the systemMetadata table under the MemoriesState key, storing the lastOnThisDayDate. This persistence ensures that if a job fails midway, the next run resumes from the correct date without regenerating existing memories.

// Acquire lock and check state
await this.databaseRepository.withLock(DatabaseLock.MemoryCreation, async () => {
  const lastOnThisDayDate = await this.systemMetadataRepository.get(SystemMetadataKey.MemoriesState);
  // Process only unprocessed dates...
});

Rolling Date Window Calculation

Immich examines a configurable window of three days before and after the current date (defined by the DAYS constant). For each unprocessed date in this range, the system triggers memory creation, allowing users to discover memories slightly early or catch up on recent ones they might have missed.

Asset Selection by Day-of-Year

The assetRepository.getByDayOfYear([ownerId], target) method performs the critical "On this day" matching. It queries assets where the month and day match the target date, regardless of year, but restricts results to the specific ownerId. The repository returns data grouped by original year as { year, assets } pairs, enabling the creation of separate memories for "2018 on this day" versus "2019 on this day."

const memories = await this.assetRepository.getByDayOfYear([ownerId], target);
// Returns: [{ year: 2018, assets: [...] }, { year: 2019, assets: [...] }]

Memory Record Creation

For each year-grouped asset collection, the service calls memoryRepository.create() with specific temporal boundaries:

  • ownerId: Ties the record to the specific user
  • type: MemoryType.OnThisDay: Categorizes the memory type
  • data: { year }: Stores the original year for UI display
  • memoryAt: The target date with the original year baked in, positioning the memory historically
  • showAt and hideAt: ISO timestamps defining the 24-hour visibility window (start and end of the target day)
await Promise.all(
  memories.map(({ year, assets }) =>
    this.memoryRepository.create(
      {
        ownerId,
        type: MemoryType.OnThisDay,
        data: { year },
        memoryAt: target.set({ year }).toISO()!,
        showAt: target.startOf('day').toISO(),
        hideAt: target.endOf('day').toISO(),
      },
      new Set(assets.map(({ id }) => id)),
    ),
  ),
);

Personalization Mechanisms

Personalization in Immich's memory system operates at multiple layers, ensuring strict data isolation between users.

Per-User Asset Isolation

Every database query filters by ownerId. The getByDayOfYear repository method accepts an array of owner IDs (though typically called with a single user in this context), and all memory repository queries (memoryRepository.search, memoryRepository.statistics) enforce user-scoped filtering. This guarantees users never see assets belonging to others, even in shared instance deployments.

Timeline Visibility Enforcement

Beyond ownership, the system respects Immich's standard visibility rules. Asset queries enforce asset.visibility = Timeline and asset.deletedAt IS NULL, ensuring that trashed photos or assets hidden from the timeline do not appear in memories. This mirrors the main timeline view, providing a consistent, personalized experience.

Key Implementation Files

The memory ecosystem spans several critical files in the server/src directory:

Summary

  • Immich generates memories via a daily MemoryGenerate background job registered in memory.service.ts using the @OnJob decorator
  • The system processes a rolling 3-day window around the current date, tracking state in systemMetadata under MemoriesState to prevent duplicates
  • Asset selection uses assetRepository.getByDayOfYear to match photos by month-day across all years, grouped by original year
  • Each memory record is bound to a specific ownerId with 24-hour visibility windows (showAt/hideAt) and type classification MemoryType.OnThisDay
  • Strict personalization is enforced through per-user asset isolation, timeline visibility filters, and user-scoped database queries

Frequently Asked Questions

How often does Immich generate new memories?

The MemoryGenerate job runs once daily as a background task. It checks a rolling window of three days before and after the current date to ensure users don't miss memories if the job fails temporarily, while state tracking in systemMetadata prevents duplicate generation.

Why do I see memories from different years grouped separately?

Immich groups assets by their original year in the getByDayOfYear repository method. Each year generates a distinct memory record with data: { year }, allowing the UI to display "On this day in 2018" and "On this day in 2019" as separate collections rather than one mixed gallery.

Can other users see my generated memories?

No. Every memory record stores your ownerId, and all repository queries filter by this ID. The memoryRepository.search method specifically requires auth.user.id, ensuring strict isolation. Even on shared Immich instances, memories remain private to the asset owner.

What happens if the memory generation job fails mid-process?

The system uses DatabaseLock.MemoryCreation to prevent concurrent runs and persists the lastOnThisDayDate in systemMetadata after each day is processed. If a failure occurs, the next job execution resumes from the last successful date stored in MemoriesState, ensuring no days are skipped or duplicated.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →