Share via

calendarView/delta returns identical event pages indefinitely — $skiptoken rotates on every response, @odata.deltaLink never emitted

HiroNakamata-5721 0 Reputation points
2026-05-07T20:53:52.16+00:00

Hello, we have an issue with the delta endpoint. A few users' Microsoft Graph calendarView/delta cursor is wedged. Every paginated response returns an identical event set with a fresh @odata.nextLink, but @odata.deltaLink is never emitted, so pagination cannot terminate. We've ruled out our pagination logic, date-window size, and request thresholds; behavior persists across thousands of consecutive requests for certain users while other users in the same tenant sync normally. We need help understanding the server-side cause and resetting the cursor. With this Outlook account, I was able to reproduce this.

Endpoint
Symptom
  • Every paginated response from calendarView/delta:
    • returns the same 278 events (identical "id" set across all pages)
    • returns a NEW @odata.nextLink whose $skiptoken differs from the previous request
    • never returns @odata.deltaLink — pagination has no terminating page
  • In one 4-hour window we observed 3,548 consecutive paginated responses matching this pattern for this user, all with the same 278-event payload.
Expected behavior

Per the calendarView/delta documentation (https://learn.microsoft.com/en-us/graph/api/event-delta), pagination should terminate with an @odata.deltaLink once all changes in the queried window have been returned. We expect either:

  • (a) Successive nextLink responses to deliver different/later changes, or
  • (b) An @odata.deltaLink terminating the round.

Neither happens for this user.

Ruled out client-side

We have ruled out client-side causes:

  • Pagination logic: we follow the server-provided @odata.nextLink literally and reject any nextLink equal to the URL we just requested.
  • Date-range size: we shrunk the queried window from 365 days to 90 days; replay behavior is unchanged.
  • Request rate / threshold: increasing our duplicate-page abort threshold from 100 → 300 → 1000 changes nothing; replays continue past any reasonable threshold.
  • Other users in the same tenant sync normally; this is per-user state.
Questions
  1. What server-side condition produces this state (rotating $skiptoken that never produces new content and never reaches @odata.deltaLink)?
  2. Is there a way to reset the delta cursor for this specific user?
  3. Does Microsoft recommend any client-side mitigation (Prefer: odata.maxpagesize, request headers, query parameters) known to recover from this state?

Impact

Our calendar integration cannot deliver any incremental changes for affected users while this state persists. The customer-visible symptom is "calendar events stop appearing in our app." since we abort our sync process upon the duplicate page detection with the aforementioned threshold. The only workaround at our layer seems that not use the delta endpoint and instead, query from other endpoint upon a change notification.

Thank you in advance for your cooperation. Hiro Nakamata, Tempo

Microsoft Security | Microsoft Graph
0 comments No comments

1 answer

Sort by: Most helpful
  1. AI answer

    2026-05-07T20:54:19.7466667+00:00

    For calendarView/delta, the behavior is governed by the general delta-query rules in Microsoft Graph:

    1. Delta query always returns either:
      • An @odata.nextLink containing a $skiptoken when there are more pages in the current round of change tracking, or
      • An @odata.deltaLink containing a $deltatoken when the round is complete.
    2. The $skiptoken and $deltatoken are opaque state tokens that encode:
      • The current snapshot of the resource set for that round.
      • Any query parameters used in the initial request (for calendarView, startDateTime and endDateTime).
      • The server’s internal paging state.
    3. For calendarView/delta specifically:
      • The method does not support $expand, $filter, $orderby, or $search.
      • $select is also not supported; the response shape is fixed to what GET /calendarView returns.
      • Query parameters other than $deltatoken and $skiptoken must be specified only in the initial request; subsequent calls must reuse the @odata.nextLink or @odata.deltaLink URL as-is.

    Given the context, the questions can be answered only at the protocol/behavior level, not per-tenant or per-user.

    1. Server-side condition for rotating $skiptoken with identical pages and no @odata.deltaLink

    From the documented behavior:

    • A @odata.nextLink with $skiptoken means the server believes there are more changes to enumerate in the current round.
    • A @odata.deltaLink with $deltatoken means the server believes the round is complete.

    The only documented, supported interpretation is that the server’s state tokens represent snapshots and paging state, and that the client must treat them as opaque. The documentation does not describe or expose any server-side condition that would intentionally produce an infinite sequence of @odata.nextLink values with identical content and no @odata.deltaLink. That pattern is not a documented or expected state.

    1. Resetting the delta cursor for a specific user

    For delta query in general, the documented way to “reset” is to start a new round by discarding the previous @odata.deltaLink/@odata.nextLink and issuing a fresh initial delta request:

    • For calendarView/delta, that means calling: GET /me/calendarView/delta?startDateTime=...&endDateTime=... again as a new round, without $deltatoken or $skiptoken.

    The documentation does not describe any additional server-side mechanism or API to reset or clear per-user delta state beyond starting a new round by omitting the state tokens.

    1. Client-side mitigations and usage constraints

    From the delta-query overview and the calendarView/delta documentation, the following client-side rules and mitigations are supported:

    • Always:
      • Specify startDateTime and endDateTime only on the initial calendarView/delta request.
      • For subsequent calls, copy and use the @odata.nextLink or @odata.deltaLink URL verbatim, without adding or changing query parameters.
    • Do not use unsupported OData parameters with calendarView/delta:
      • $select is not supported for calendarView/delta.
      • $expand, $filter, $orderby, and $search are not supported.
    • Prefer: odata.maxpagesize={x} is allowed and can be used to control page size, but it does not change the semantics of when @odata.deltaLink is emitted.
    • For all delta queries:
      • $deltatoken and $skiptoken must be treated as opaque; do not attempt to interpret or modify them.
      • Any query parameters used in the initial request are encoded into the tokens and automatically carried forward; do not repeat or alter them in subsequent requests.

    The documentation does not provide a specific mitigation for a state where the server continually returns identical pages with rotating $skiptoken and never emits @odata.deltaLink. The only documented recovery pattern is to start a new delta round by discarding the existing state token and issuing a fresh initial delta request for the desired time window.


    References:

    AI-generated content may be incorrect. Read our transparency notes for more information.

    Was this answer helpful?

Your answer

Answers can be marked as 'Accepted' by the question author and 'Recommended' by moderators, which helps users know the answer solved the author's problem.