What I learned

GORM’s clause.Returning{} causes every UPDATE (and CREATE) to emit UPDATE ... RETURNING * instead of a plain UPDATE. The result is silently discarded if the caller doesn’t use it — but PostgreSQL still assembles and streams the full row back, changing the transaction round-trip.

Warning

Silent costclause.Returning{} is often added for convenience in one call path and left in permanently. If the return value isn’t used, it’s pure overhead that also creates a TCP-stall vulnerability (see postgres-returning-tcp-session-stall).

Remove it when the return value isn’t used

// Before — emits UPDATE ... RETURNING *; result never read
return db.Model(&models.EntityParty{}).
    Clauses(clause.Returning{}).
    Where(...).
    Updates(...).Error
 
// After — plain UPDATE; commits in one round-trip
return db.Model(&models.EntityParty{}).
    Where(...).
    Updates(...).Error

Add a context timeout as a safety net

If the DB call stalls for any reason, a context timeout lets the goroutine cancel after a bounded time. The driver sends a cancellation to PostgreSQL and the caller moves on.

func (r *EntityPartyRepository) UpdateLastSyncTime(
    db *gorm.DB,
    partyId, updateFieldName string,
    lastSyncAt time.Time,
) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    return db.WithContext(ctx).
        Model(&models.EntityParty{}).
        Where(...).
        Updates(...).Error
}

Note

The timeout protects the goroutine, not the PostgreSQL session. If the TCP connection is already dead, PostgreSQL won’t know until keepalive fires regardless of the context cancellation. But without RETURNING *, PostgreSQL has nothing to wait for anyway — the session resolves immediately on commit.

See also