the directus-sync push that kept failing
Past midnight. Needed to push schema changes, new collections for conversation artifacts, verification topics, some field additions. Standard directus-sync push. Should’ve been five minutes.
Took three hours.
the timeout
First push attempt timed out. Directus was unresponsive. Checked the database and found a zombie process from September. A DELETE FROM directus_activity query that had been running (or stuck) for months. PID 1954. Just sitting there, holding locks.
Problem: we’re on DigitalOcean managed Postgres. No superuser access. Can’t pg_terminate_backend() on someone else’s process. No restart button in the dashboard. Had to work around it.
The directus_activity table was massive and bloated. Truncated both directus_revisions and directus_activity:
TRUNCATE directus_revisions, directus_activity CASCADE;Directus doesn’t need these for core functionality. They power the revision history UI. Acceptable trade-off at 1am.
phantom indexes
Directus came back. Tried the sync again.
drop index "conversation_chunk_timestamp_index" -index "conversation_chunk_timestamp_index" does not existThen another:
drop index "directus_activity_collection_index" -index "directus_activity_collection_index" does not existThe sync snapshot (the local state file that directus-sync uses to diff against production) referenced indexes that didn’t exist in the actual database. Every push tried to drop them, failed, and aborted the entire schema application.
Fix was stupid but effective: create the missing indexes so the sync could drop them:
CREATE INDEX conversation_chunk_timestamp_index ON conversation_chunk(timestamp);CREATE INDEX directus_activity_collection_index ON directus_activity(collection);CREATE INDEX directus_presets_collection_index ON directus_presets(collection);CREATE INDEX directus_revisions_collection_index ON directus_revisions(collection);CREATE INDEX directus_revisions_parent_index ON directus_revisions(parent);Then the push, which would drop and recreate them as part of its schema reconciliation.
constraint renames
The sync diff also wanted to rename constraints, changing _fkey suffixes to _foreign, and flip some on_delete behaviors from SET NULL to CASCADE. These are Directus internals, not things we changed intentionally. Different Directus versions use different naming conventions for constraints, and the sync tool picks up on these differences.
This is the fundamental tension with directus-sync: it tries to make your local snapshot the source of truth, but Directus itself evolves its internal schema conventions between versions. You end up with phantom diffs that aren’t real changes, just naming inconsistencies.
what I’d do differently
Keep the sync snapshot closer to production. We’d let it drift because pulling from production felt unnecessary when “we know what we changed.” That assumption breaks the moment Directus does anything internally that your local snapshot doesn’t know about.
Also: monitor for zombie queries. A three-month-old stuck query is not normal, and it was probably causing subtle performance issues we attributed to other things. We now have basic query monitoring that flags anything running longer than an hour.
The whole thing was solvable. Nothing was fundamentally broken. But stale state + zombie processes + schema naming inconsistencies turned a five-minute deploy into a late-night debugging session. GitOps with Directus works. It just has sharp edges that bite you at the worst times.