# Daily puzzle scores: which day they belong to and how they get lost

> In a game started at 23:50 and finished at 00:05, the puzzle date, the submission time, and the leaderboard window can disagree. How far to guarantee delivery.

- Canonical: https://jaemyeong.com/en/blog/daily-leaderboard-date-boundary-submission-reliability/
- Published: 2026.08.06
- Updated: 2026.10.04
- Category: IT/기술
- Tags: #iOS, #Android, #GameKit, #Google Play Games, #Testing

When the result of a once-a-day puzzle is sent to both Game Center and Google Play Games, the date becomes a problem for a game that crosses midnight. In a game started at 23:50 and finished at 00:05, the day the puzzle belongs to, the day the SDK received the submission, and the range the leaderboard screen shows can all disagree.

Delivery is a problem too. The completion record and the statistics can remain on the device while the external score is lost. The opposite problem also exists: asking for sign-in in the middle of completion ties game progress to authentication.

The basis is the official Apple and Google documentation, which I reopened and checked on October 4, 2026. What those documents state is the behavior of the SDKs and the servers. The submission condition, where the sign-in screen goes, the in-memory slot, and the outbox are app-side design on top of that, not requirements of the documentation. The midnight case is a made-up example. This post does not give measured values, run logs, or SDK versions.

## Three values that all look like a date

Three values look like the same "date" but have different owners. `puzzle_day` is the puzzle's date, decided by the app. `submitted_at` is the submission time, decided by the app and the device. `leaderboard_window` is the range used for queries, decided by the SDK.

The puzzle date is set when the game is created and does not change within the session. The submission time can change on every retry. Nothing guarantees that the SDK's window matches midnight on the device. So the date is not calculated again at completion. The value stored in the session is used.

This example shows how the three values differ in a game that crosses midnight.

```text
퍼즐 생성: 2026-08-05 23:50 → puzzle_day = 2026-08-05
게임 완료: 2026-08-06 00:05 → submitted_at = 2026-08-06 00:05
제출 메타데이터                    → 2026-08-05
```

The three Korean labels mean "puzzle created," "game completed," and "submission metadata."

## Extra values do not change the window

On Android, the puzzle date in `yyyy-MM-dd` form can be carried in `scoreTag`. `scoreTag` is an optional argument of `submitScore` in [LeaderboardsClient](https://developers.google.com/android/reference/com/google/android/gms/games/LeaderboardsClient). It allows only URI-safe characters and up to 64 characters. The documentation defines this value only as optional metadata. Using it to say which puzzle date a score came from is the app's own choice. It does not prevent duplicates per date and does not filter the screen. Putting 2026-08-05 in the tag does not split the rows of `TIME_SPAN_DAILY` by date.

`context` on Apple is the same. The `context` of [submitScore](https://developer.apple.com/documentation/gamekit/gkleaderboard/submitscore(_:context:player:leaderboardids:completionhandler:)) is an extra value whose meaning the app defines, so how to express a date in it is the app's responsibility. It is not a way to control `timeScope`. If only `context: 0` was sent, the puzzle date cannot be worked out from the score data. The fact that this value means different things on the two platforms should be written into the product requirements.

## The day that the default screens show

[Play Games leaderboards](https://developer.android.com/games/pgs/leaderboards) offer daily, weekly, and all-time, and the daily boundary is midnight UTC-7 all year. Apple's [today scope](https://developer.apple.com/documentation/gamekit/gkleaderboard/timescope-swift.enum/today) is the last 24 hours. When an occurrence of an Apple [recurring leaderboard](https://developer.apple.com/documentation/gamekit/creating-recurring-leaderboards) changes depends on the [recurrence settings in App Store Connect](https://developer.apple.com/help/app-store-connect/reference/game-center/leaderboards/). Neither default daily screen guarantees a match with `puzzle_day`.

Take an app whose puzzle changes at midnight in Seoul. Scores for the new puzzle and for the previous day's puzzle can fall into one Google daily window. Apple's today scope can also hold scores from both days. This comes from a different contract for the displayed range, and editing the tag does not solve it.

There are three paths, depending on the requirement.

- If the SDK's daily screen is enough, use the extra value and the default UI.
- If occurrences need to be told apart on Apple, consider a recurring leaderboard.
- If both platforms must use the same calendar rule and the same query rule, a separate store and a custom UI are needed.

The last path brings costs for operations, anti-cheating, and privacy management. For a simple daily competition, I judged that it does not need to be introduced in advance. That judgment is my reading, not something the documentation says.

## When to submit and when not to

The SDKs do not define when to submit. The policy below is an example of what the app decides: a score is submitted only when three conditions are all met.

```text
submit = won && daily && authenticated
```

In free mode, after a loss, or without authentication, nothing is sent. When the authentication state is uncertain, only a check that shows no screen is allowed. The sign-in screen belongs on the path where the user presses the leaderboard button. A disabled service, a signed-out user, or a canceled sign-in must not make completion, statistics, or the screen transition fail.

The device has its own duties at completion. It prevents the terminal transition from being handled twice, deletes the resume data, and updates the statistics and the daily completion record. The external submission is a separate attempt, and its failure does not roll completion back.

The phrase "local first" needs care here. The phrase alone does not say that the write to disk finishes before the submission. On Android, if the submission starts right after an asynchronous write is scheduled, the order in which the two finish is not defined. If the disk record has to be settled first, the design needs to wait for the save to complete.

## Holding one score for a short while

On Android, a score can arrive in the short moment when no UI host is connected. One design for that case holds at most one score in memory. This is a design on the app side, not a feature of the SDK. If A is held and B arrives, B replaces A. When an Activity connects, the score is taken out, authentication is checked, and it is submitted once.

```text
Activity 없음
  score A 보류
  score B 도착 → A를 B로 교체
Activity 연결
  B를 꺼내 인증 확인 → 제출 시도
```

In the block, the lines read: no Activity, score A is held, score B arrives and replaces A, an Activity connects, and B is taken out, authentication is checked, and a submission is attempted.

The in-memory slot exists to reduce short lifecycle races, and its limits are clear. If the process dies, the held score is gone. If authentication is false or the check fails after the score is taken out, it is not put back. If iOS has no equivalent in-memory slot, the chance that a score is delivered also differs between the two platforms.

Keeping the latest item is a rule inside the device only. Google applies a score only when it is better than the value on the server, and Apple picks the best score or the most recent score depending on the setting. The app choosing the latest item does not mean the server adopts that record.

The call itself differs too. `submitScore` does not report a result, and `submitScoreImmediate` returns a `Task`. With the call that reports nothing, the app cannot tell whether the submission succeeded. For the one that returns a `Task`, too, the documentation says nothing about sending again after the app restarts.

## How far to guarantee delivery

Splitting the delivery level into three is also my own classification. The documentation does not make this distinction.

- One direct attempt: send once within the session. It costs the least and fits a feature that can afford to miss a score.
- One slot in memory: keep the latest item while there is no host. The cost is low, and it fits short lifecycle races.
- Durable outbox: the score can be sent again after a restart. The cost is high, and it fits cases where a lost submission affects rewards or contest results.

With a durable outbox, the payload is written to disk first. A stable submission ID, `puzzle_day`, the score, and the target leaderboard are stored. Only items with a success response are deleted, and the remaining items are restored on restart. Backoff and success confirmation are part of it. The server keeping an existing better record and the app deciding to stop resending are judged separately. Storing a submission ID does not mean the SDK removes duplicates or processes a score exactly once.

If loss is accepted, the queue is not extended into a general-purpose one. The conditions under which a score is dropped are written into the documentation and the tests.

## Expected values to pin with tests

The expected values below are a plan: keep the condition and the holding policy as pure functions and pin them with tests. This post does not include results from a run.

- A win in free mode or a loss in daily mode gives 0 submissions.
- A daily win without authentication gives 0 submissions and no sign-in screen.
- Playing from 23:50 to 00:05 keeps the session's puzzle date.
- With no Activity, A followed by B keeps only B.
- If the process dies while a score is held, nothing is submitted after restart.
- If authentication fails after the score is taken out, it is not held again.
- If the remote request fails, the completion record and statistics remain.
- For a latest score lower than the previous one, the device's latest choice and the server's best choice are checked separately.
- If the terminal event arrives again, completion is recorded once and submission is attempted once.
- The default daily and today queries use the SDK's time range, not the extra value.

Unit tests prove only the app's branches and the payload. They are not evidence that a score actually reached the server. Canceled sign-in, offline use, and display errors of the leaderboard intent and the access point have to be checked separately in an integrated environment. The different meaning of a day on the default screens, the loss of the in-memory score, the call whose result is unknown, and the order in which the disk write completes all remain as limits of this design.
