Timestamp Management#
Dify's codebase uses two distinct mechanisms for storing timestamps — application-generated naive UTC datetimes and database-generated server timestamps — sometimes on the same columns. Understanding when each fires, and where they can diverge, is essential for debugging ordering bugs, updated_at drift, and DST-related parsing issues.
Core Utility: naive_utc_now()#
naive_utc_now() in api/libs/datetime_utils.py is the canonical Python-side timestamp source. It calls datetime.datetime.now(datetime.UTC) then strips the timezone with .replace(tzinfo=None), returning a naive UTC datetime. The underlying _now_func is a swappable callable specifically to allow time mocking in tests .
A companion, ensure_naive_utc(), normalizes any timezone-aware input to naive UTC before persistence. parse_time_range() handles user-submitted local time strings, converting them to UTC with DST-ambiguity and non-existent-time guard logic.
Model-Level Defaults: Two Patterns#
DefaultFieldsMixin / DefaultFieldsDCMixin (new-style models)#
Models that inherit from DefaultFieldsMixin or DefaultFieldsDCMixin get:
created_at—default=naive_utc_now(Python-side) +server_default=func.current_timestamp()(DB-side fallback)updated_at— same defaults, plusonupdate=func.current_timestamp()
The Python-side default fires when SQLAlchemy creates the INSERT statement; the server default is a fallback for raw SQL inserts that bypass the ORM.
Legacy Conversation and Message models#
The Conversation and Message models do not inherit DefaultFieldsMixin. Their created_at and updated_at columns are defined with only server_default=func.current_timestamp() and onupdate=func.current_timestamp() — no Python-side default. This means on INSERT the database clock sets the value, not the application clock.
Explicit Assignments in Task Pipelines#
Despite the onupdate=func.current_timestamp() directive on Message.updated_at, both task pipelines explicitly overwrite it with naive_utc_now() during message finalization:
EasyUIBasedGenerateTaskPipeline._save_message()—message.updated_at = naive_utc_now()AdvancedChatAppGenerateTaskPipeline._save_message()—message.updated_at = naive_utc_now()
ConversationService.rename() does the same for conversations: conversation.updated_at = naive_utc_now().
Similarly, MessageBasedAppGenerator._init_generate_records() explicitly sets conversation.updated_at = naive_utc_now() when an existing conversation gets a new message, rather than relying on the DB onupdate hook.
The rationale: SQLAlchemy's onupdate only fires if a column other than updated_at is also being changed in the same UPDATE. Explicit assignment guarantees the column is always written, avoiding a silent no-op when only updated_at itself needs refreshing.
Pitfalls and Discrepancies#
| Scenario | Source | Risk |
|---|---|---|
New-style models (use DefaultFieldsMixin) | Python naive_utc_now() | Application clock; consistent with Python comparisons |
Legacy Conversation/Message on INSERT | DB func.current_timestamp() | DB clock may differ from app server clock by milliseconds to seconds |
onupdate path (no explicit assignment) | DB clock | App-side objects don't reflect the new value until re-fetched |
Explicit naive_utc_now() assignment in pipelines | Python | Consistent, but bypasses onupdate; value already written as payload |
parse_time_range() user input | pytz conversion to UTC | DST edge cases handled; assumes naive timestamps already in UTC |
Key hazard: Sorting or comparing timestamps across models that use different sources can produce off-by-milliseconds ordering surprises. If code reads message.updated_at immediately after the session.commit() without a refresh, the in-memory object may hold a stale value (the Python-assigned one) while the database has the onupdate-generated value — or vice versa.
Key Files#
| File | Role |
|---|---|
api/libs/datetime_utils.py | naive_utc_now(), ensure_naive_utc(), parse_time_range() |
api/models/base.py | DefaultFieldsMixin, DefaultFieldsDCMixin — standard timestamp columns |
api/models/model.py | Conversation and Message legacy timestamp columns |
api/core/app/task_pipeline/easy_ui_based_generate_task_pipeline.py | Explicit updated_at on EasyUI message save |
api/core/app/apps/advanced_chat/generate_task_pipeline.py | Explicit updated_at on AdvancedChat message save |
api/core/app/apps/message_based_app_generator.py | Explicit updated_at on conversation update |
api/services/conversation_service.py | Explicit updated_at on rename |