baomidou/mybatis-plus · critical · RuntimeException

Clock moved backwards. Refusing to generate id for %d milli

Error message

Clock moved backwards.  Refusing to generate id for %d milliseconds

What it means

Sequence.nextId() is mybatis-plus's Snowflake-style ID generator. If the current timestamp is older than the last issued timestamp (clock went backwards), it tolerates at most 5 ms: it sleeps 2x the offset and re-reads the clock. This site throws RuntimeException('Clock moved backwards... N milliseconds') when, after that wait, time is STILL behind lastTimestamp — i.e. the small drift did not recover. Refusing to generate keeps IDs monotonic and collision-free.

Source

Thrown at mybatis-plus-core/src/main/java/com/baomidou/mybatisplus/core/toolkit/Sequence.java:180

        return id;
    }

    /**
     * 获取下一个 ID
     *
     * @return 下一个 ID
     */
    public synchronized long nextId() {
        long timestamp = timeGen();
        //闰秒
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    Thread.sleep(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            } else {
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
            }
        }

        if (lastTimestamp == timestamp) {
            // 相同毫秒内,序列号自增
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 同一毫秒的序列数已经达到最大
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 不同毫秒内,序列号置为 1 - 2 随机数

View on GitHub (pinned to bf67d90747)

Solutions

  1. Ensure NTP/chronyd runs in slew mode (gradual adjustment) rather than step mode on hosts generating IDs.
  2. Avoid VM snapshot/resume patterns for ID-generating services, or re-sync time immediately after resume.
  3. If backward steps are expected in your environment, set mybatis-plus's IdentifierGenerator to a custom implementation (e.g. one that waits until time passes lastTimestamp instead of throwing).
  4. Retry the insert once the clock has moved past lastTimestamp (a few ms) — the exception is transient by nature.

Example fix

// before: default snowflake throws on small backward clock steps
@IdType(IdType.ASSIGN_ID)
private Long id;

// after: tolerate clock skew by supplying a patient generator
@Configuration
public class IdConfig {
    @Bean
    public IdentifierGenerator identifierGenerator() {
        return new IdentifierGenerator() {
            private final Sequence seq = new Sequence();
            @Override
            public Long nextId(Object entity) {
                long ts, last;
                do { ts = System.currentTimeMillis(); } while (ts < (last = lastSeen.get()) && !lastSeen.compareAndSet(last, ts));
                lastSeen.set(ts);
                return seq.nextId();
            }
            private final AtomicLong lastSeen = new AtomicLong();
        };
    }
}
Defensive patterns

Strategy: retry

Validate before calling

long now = System.currentTimeMillis();
long lastIssued = readLastIssuedTimestamp();
if (now < lastIssued) {
    // wait out the skew before inserting
    Thread.sleep(lastIssued - now);
}

Try / catch

for (int attempt = 0; attempt < 3; attempt++) {
    try {
        return insert(entity); // ASSIGN_ID snowflake insert
    } catch (RuntimeException e) {
        if (!isClockMovedBackwards(e) || attempt == 2) throw e;
        sleepQuietly(10L << attempt);
    }
}

Prevention

When it happens

Trigger: System clock steps backwards by 1-5 ms and does not catch up within 2x the offset sleep (continued backward drift, e.g. NTP slewing downward, VM pause/resume, or a host clock being actively corrected). Sequence.nextId() is called during this window (any insert that assigns an ASSIGN_ID primary key).

Common situations: NTP stepping the clock back; virtual machines resumed from snapshot/suspend with stale clocks; containers inheriting a corrected host clock mid-request; bare-metal hosts after manual date changes.


AI-assisted analysis of baomidou/mybatis-plus@bf67d90747 (2026-08-14). Data as JSON: /api/errors/1b8391a497949e4e. Report an issue: GitHub.