Skip to content

[Bug] startTimerWithFixedDelay is implemented with scheduleAtFixedRate, so a slow handler accumulates ticks — the exact behaviour the fixed-delay name rules out #921

Description

@pathosDev

Problem

TimerScheduler.startTimerWithFixedDelay is implemented as fixed-rate. It delegates to Scheduler.scheduleAtFixedRate, which is a raw setInterval.

The distinction is the entire reason the API exists. Fixed-delay measures the next interval from the end of the previous execution, specifically so that a handler slower than the interval cannot accumulate a backlog. Fixed-rate fires on wall-clock cadence regardless of whether the previous tick was handled. With the current implementation a slow actor's ticks pile into its mailbox — and with the default drop-head bounded mailbox those accumulated ticks then evict the actor's real work.

The call site reads correctly and the JSDoc does not warn, so this fails silently for exactly the workload the API was chosen to protect.

Evidence

src/internal/ActorCell.ts:1238-1249
  startTimerWithFixedDelay(
    key: string,
    message: TMessage,
    intervalMs: number,
    initialDelayMs: number = intervalMs,
  ): void {
    this.cancel(key);
    const handle = this.cell.system.scheduler.scheduleAtFixedRate(
      initialDelayMs, intervalMs, this.cell.self, message, null,
    );
    this.handles.set(key, handle);
  }

and the target, which is interval-based:

src/Scheduler.ts:58-60
  /** Deliver a message repeatedly at a fixed interval, after an initial delay. */
  scheduleAtFixedRate<T>(

src/ActorContext.ts:264 documents the method without noting the discrepancy.

Proposal

Implement real fixed-delay: schedule the next tick from inside the completion of the previous one (scheduleOnce re-armed after the message is handled), rather than from a free-running interval. If that is not wanted, rename the method to match scheduleAtFixedRate — but shipping the fixed-delay name with fixed-rate behaviour will burn someone whose handler occasionally slows down, and the symptom (evicted work) will not point at the timer.

Acceptance sketch

  • With a handler slower than the interval, startTimerWithFixedDelay produces one pending tick at a time, not a backlog.
  • startTimerWithFixedRate (or the existing scheduler method) remains available for callers who genuinely want wall-clock cadence.
  • The JSDoc states which of the two semantics each method has.
  • A test drives a slow handler and asserts the mailbox does not accumulate ticks.

Verification status

Found in the ten-lens production-readiness review of 2026-08-05 (v0.13.0) and re-verified before filing: confirmed by reading the delegation chain at the cited lines. Not reproduced at runtime because the mechanism is a direct call into setInterval.

Part of the production-readiness review batch — tracked in #913.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingpriority: mediumUseful, not urgentproduction-goalBlocks or defines the path to production readiness

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions