staff-engineering-skills-clock-skew

작성자: triggerdotdev

시계가 동기화되거나 단조적이라고 가정하여 발생하는 버그를 방지합니다. 여러 머신 간에 타임스탬프를 비교하거나, 기간을 측정하거나, 설정하는 코드를 작성할 때 사용하세요…

npx skills add https://github.com/triggerdotdev/staff-engineering-skills --skill staff-engineering-skills-clock-skew

Clock Skew Trap

You used timestamps for ordering. Time disagreed. Before using Date.now() for anything other than logging or display, ask: does correctness depend on this timestamp being accurate relative to another machine's clock, or relative to a previous reading on this machine?

Two Kinds of Time

Wall clockMonotonic clock
What it isCurrent time of day (NTP-synchronized)Counter that only moves forward
Can go backward?Yes (NTP corrections, clock steps)No, by definition
Comparable across machines?Only within clock skew margin (typically 1-100ms)No -- only meaningful within one process
Use forLogging, display, human-readable timestampsDurations, timeouts, elapsed time measurement
APIDate.now(), time.time(), System.currentTimeMillis()process.hrtime.bigint(), time.monotonic(), time.Since()

The rule: Use wall clock for humans. Use monotonic clock for measurement. Use logical clocks for distributed ordering.

Detection: When You're Misusing Time

Stop and fix if you see:

  1. Date.now() to measure a duration -- end - start can go negative if NTP adjusts the clock between the two calls. Use a monotonic clock.
  2. Timestamps compared across machines for ordering -- "A happened before B" from different servers' Date.now(). If skew is 50ms and events are 30ms apart, you can't know the order. Use logical clocks or a centralized sequence.
  3. Absolute timestamp expiry shared across machines -- { expiresAt: Date.now() + 60000 } written by Machine A, checked by Machine B. If B's clock is ahead, data expires early. Use relative TTLs.
  4. Last-write-wins using wall clock timestamps -- two writes within the skew window have undefined ordering. The "winner" is whichever machine's clock runs fast, not which write actually happened last.
  5. Distributed lock expiry using wall clock -- if (Date.now() > lockExpiresAt) checked on a different machine than acquired it. Skew makes the lock appear expired early (two holders) or late (delayed release).
  6. Deduplication by timestamp proximity -- "ignore events within 100ms." 150ms of skew makes simultaneous events look 150ms apart (not deduped) or 150ms-apart events look simultaneous (wrongly deduped). Dedupe by unique ID.

When Wall Clock Is Fine

// Logging, display, analytics: humans read it; cross-machine order/sub-second accuracy isn't critical
logger.info("Request completed", { timestamp: new Date().toISOString() });
await analytics.track("page_view", { timestamp: Date.now() }); // events/hour survives 100ms skew

// Single-machine record, not used for distributed ordering
const createdAt = new Date();

Wall clock is fine when correctness doesn't depend on cross-machine comparison or exact duration measurement.

Patterns

Monotonic clock for durations

// Node.js -- always non-negative, even if NTP adjusts the wall clock mid-operation
const start = process.hrtime.bigint();
await doExpensiveOperation();
const elapsedMs = Number(process.hrtime.bigint() - start) / 1_000_000;

Equivalents: Python time.monotonic() (diff is always non-negative); Go time.Since(time.Now()) (uses the monotonic component automatically).

Use monotonic clocks for: timeouts, latency measurement, rate-limiting windows, any end - start. They're only valid within a single process -- never compare across machines.

Relative TTLs instead of absolute expiry

// BAD: absolute expiry shared across machines -- B may disagree on when "now" is
await redis.pexpireat("session", Date.now() + 3600_000); // Machine A's "1 hour from now"

// GOOD: pass durations; let each system compute expiry from its own clock
await redis.expire("session", 3600);             // Redis's own clock
await redis.setex(key, ttlSeconds, payload);     // same system sets and checks the timer

The principle: pass durations (seconds, ms) between systems, not absolute timestamps.

Logical clocks for distributed ordering

To order events across machines, use a logical clock that guarantees causal ordering, not wall clock.

class LamportClock {
  private counter = 0;
  tick(): number { return ++this.counter; }
  receive(senderClock: number): number {
    this.counter = Math.max(this.counter, senderClock) + 1;
    return this.counter;
  }
}

const event = {
  type: "order.created",
  logicalTime: clock.tick(),           // ordering: monotonic, causally consistent
  wallClock: new Date().toISOString(), // humans: display/debug only
};

Lamport clocks guarantee: if A causally precedes B, then A's logical time < B's. They don't identify concurrent events (use vector clocks for that). For most apps, a centralized sequence generator (DB auto-increment, Redis INCR) is simpler and gives a total order.

Hybrid logical clocks (HLC)

Combines wall clock (rough real-time correspondence) with a logical counter (causal ordering); used by CockroachDB. HLC timestamps sort first by wall time, then by logical counter -- so they roughly track real time while keeping causally related events ordered even when wall clocks collide.

class HybridLogicalClock {
  private physicalTime = 0;
  private logical = 0;

  now(): { wallMs: number; logical: number } {
    const pt = Date.now();
    if (pt > this.physicalTime) { this.physicalTime = pt; this.logical = 0; }
    else { this.logical++; }
    return { wallMs: this.physicalTime, logical: this.logical };
  }

  receive(remote: { wallMs: number; logical: number }): { wallMs: number; logical: number } {
    const maxPt = Math.max(Date.now(), this.physicalTime, remote.wallMs);
    if (maxPt === this.physicalTime && maxPt === remote.wallMs) {
      this.logical = Math.max(this.logical, remote.logical) + 1;
    } else if (maxPt === this.physicalTime) {
      this.logical++;
    } else if (maxPt === remote.wallMs) {
      this.logical = remote.logical + 1;
    } else {
      this.logical = 0;
    }
    this.physicalTime = maxPt;
    return { wallMs: this.physicalTime, logical: this.logical };
  }
}

Fencing tokens for distributed locks

Don't rely on clock-based expiry alone. Hand out a monotonically increasing token at acquisition; the protected resource rejects stale ones.

const { token } = await acquireLockWithToken("resource-123"); // token monotonically increases
await protectedService.write({ data: newData, fencingToken: token });

// The protected service is the final arbiter, not the clock
async function write(req: { data: Data; fencingToken: number }) {
  if (req.fencingToken <= this.lastSeenToken) {
    throw new Error("Stale fencing token -- lock was superseded");
  }
  this.lastSeenToken = req.fencingToken;
  await db.save(req.data);
}

Even if skew makes two processes believe they hold the lock, only the highest token can write.

Anti-Patterns

// Duration with wall clock: can be negative
const elapsed = Date.now() - start; // Use process.hrtime.bigint() instead

// Cross-machine ordering with wall clock: undefined within skew window
events.sort((a, b) => a.timestamp - b.timestamp); // Timestamps from different machines

// Absolute expiry across machines: skew causes early/late expiry
await cache.set(key, { expiresAt: Date.now() + 60000 }); // Pass ttlSeconds instead

// Last-write-wins with wall clock: "last" is undefined
const winner = writes.reduce((a, b) => a.timestamp > b.timestamp ? a : b);

// Lock expiry with wall clock: two holders possible
if (Date.now() > lockExpiresAt) acquireLock(); // Different machine's clock

Related Traps

  • Race Conditions -- clock skew can create race conditions in distributed locks. Two processes both believe they hold an "exclusive" lock because their clocks disagree on whether the TTL has expired. Fencing tokens prevent the downstream corruption even when the lock fails.
  • Retry Storms -- timeout calculations using wall clock can fire early (clock jumps forward) or never fire (clock jumps backward). Use monotonic clocks for all timeout logic.
  • Hot Partitions -- time-based partition keys interact with clock skew at boundaries. Near midnight, machines with different clocks write to different date partitions, creating inconsistency.
  • Idempotency -- deduplication windows based on timestamps are unreliable when events come from machines with different clocks. Use unique IDs for deduplication, not timestamp proximity.

triggerdotdev의 다른 스킬

trigger-dev-tasks
triggerdotdev
Trigger.dev 백그라운드 작업과 워크플로우를 작성, 설계 또는 최적화할 때 이 스킬을 사용하세요. 여기에는 안정적인 비동기 작업 생성, AI 구현 등이 포함됩니다.
official
trigger-authoring-chat-agent
triggerdotdev
@trigger.dev/sdk/ai의 chat.agent를 사용하여 지속형 AI 채팅 에이전트를 작성하고 실행합니다: 턴별 실행 루프, ...chat.toStreamTextOptions()를 반드시 펼쳐야 하는 이유
official
trigger-agents
triggerdotdev
Trigger.dev를 사용한 AI 에이전트 패턴 - 오케스트레이션, 병렬화, 라우팅, 평가자-최적화기, 인간-인-더-루프. LLM 기반 작업을 구축할 때 사용합니다…
official
trigger-config
triggerdotdev
Trigger.dev 프로젝트를 trigger.config.ts로 구성합니다. Prisma, Playwright, FFmpeg, Python용 빌드 확장을 설정하거나 배포를 사용자 지정할 때 사용합니다…
official
trigger-cost-savings
triggerdotdev
Trigger.dev 작업, 일정 및 실행을 분석하여 비용 최적화 기회를 찾습니다. 지출 절감, 비용 최적화, 사용량 감사, 적정 규모 조정 등을 요청받을 때 사용하세요.
official
trigger-realtime
triggerdotdev
Trigger.dev 작업 실행을 프론트엔드와 백엔드에서 실시간으로 구독합니다. 진행률 표시기, 라이브 대시보드, 스트리밍 AI/LLM 응답 등을 구축할 때 사용하세요.
official
trigger-setup
triggerdotdev
프로젝트에 Trigger.dev를 설정합니다. Trigger.dev를 처음 추가하거나, trigger.config.ts를 생성하거나, trigger 디렉토리를 초기화할 때 사용하세요.
official
trigger-tasks
triggerdotdev
AI 에이전트, 워크플로우 및 지속적인 백그라운드 작업을 Trigger.dev로 구축하세요. 작업 생성, 작업 트리거, 재시도 처리, 크론 작업 예약 시 사용하거나...
official