Account Activity Tracking#
Dify tracks when accounts were last active via the last_active_at column on the Account model. The system throttles DB writes using Redis and surfaces the value in the UI as a human-readable relative time string.
Data Model#
Account.last_active_at is a non-nullable DateTime column initialized by the database clock via server_default=func.current_timestamp() . Unlike created_at/updated_at on newer models that use DefaultFieldsMixin (which also set a Python-side default via naive_utc_now()), last_active_at has no Python-side default β its initial value always comes from the DB clock.
Clock consistency note: Because the initial write uses the DB clock and subsequent updates use the Python application clock (
naive_utc_now()), there is a potential off-by-milliseconds discrepancy between the first value and later updates. See the Timestamp Management knowledge article for the broader pattern.
Update Flow#
last_active_at is refreshed on every authenticated request via AccountService.load_user(), which calls _refresh_account_last_active() before returning. Two guards prevent excessive DB writes:
Guard 1 β In-memory staleness check#
Before touching Redis or the database, the method computes refresh_before = now - 10 minutes and bails out if account.last_active_at >= refresh_before . This short-circuits the Redis round-trip on most requests.
Guard 2 β Redis distributed lock (10-minute TTL)#
If the in-memory check passes, _should_refresh_account_last_active() issues a Redis SET β¦ EX β¦ NX on key account_last_active_refresh:{account_id} with a 10-minute expiry . The constants are defined at module level:
ACCOUNT_LAST_ACTIVE_REFRESH_PREFIX = "account_last_active_refresh:"ACCOUNT_LAST_ACTIVE_REFRESH_INTERVAL = timedelta(minutes=10)
SET NX is atomic β only one worker across all processes can win the lock and perform the DB write. The method is decorated with @redis_fallback(default_return=True), so if Redis is unavailable the update proceeds anyway .
DB write#
When both guards pass, a targeted UPDATE is issued β not a full ORM flush :
UPDATE accounts
SET last_active_at = <now>, updated_at = CURRENT_TIMESTAMP
WHERE id = :id AND last_active_at < :refresh_before
The WHERE last_active_at < refresh_before predicate makes the write idempotent against concurrent races: whichever worker wins the Redis lock will perform the update; any lagging worker whose Redis key expired between the check and the write will be stopped by this predicate.
Frontend Display#
The useFormatTimeFromNow React hook formats last_active_at as a locale-aware relative string (e.g., "3 minutes ago") . It uses the dayjs relativeTime plugin and resolves the locale from useLocale() via a localeMap lookup, with 'en' as fallback . The hook bundles locale data for 20+ languages .
Key Files#
| File | Role |
|---|---|
api/models/account.py | Account.last_active_at column definition |
api/services/account_service.py | _refresh_account_last_active, Redis guard, DB update |
web/hooks/use-format-time-from-now.ts | Frontend dayjs.fromNow() hook |
api/libs/datetime_utils.py | naive_utc_now() β Python-side timestamp source for updates |