# Google's OAuth 'Testing' mode expires refresh tokens in 7 days. Publish the consent screen before you schedule anything.

> Source: <https://dev.to/ko-hi/googles-oauth-testing-mode-expires-refresh-tokens-in-7-days-publish-the-consent-screen-before-24hm>
> Published: 2026-09-16 22:30:18+00:00

**TL;DR** — If your Google Cloud OAuth consent screen is still in **Testing**, every refresh token it issues dies after seven days. That is documented behavior, not a bug. My unattended YouTube uploader ran fine for a week and then stopped at 06:45 on a Saturday. The fix is not "re-authenticate"; it is "publish the app", and publishing has prerequisites that took an hour, not a minute.

You scheduled it. It ran for a week. Then it stopped.

That is the whole shape of this failure, and if you have a cron job, a GitHub Action, or a Task Scheduler entry that talks to a Google API with a refresh token you minted on your laptop, it is probably sitting in your future too.

My YouTube Shorts pipeline is the one part of my setup that runs with nobody watching: Claude Code writes the script, a renderer makes the video, and a PowerShell runner uploads it at 14:00 and 21:00 every day. On September 12 at 06:45 the refresh token expired. It was exactly seven days old.

The same morning, before the first scheduled slot, I added a small script called `check_youtube_token.js` in front of the runner. It tries to refresh the token; if that fails, the runner writes `SKIP: waiting for YouTube re-auth` to its log and exits without launching anything. So the outage was loud rather than silent: three skipped slots that day, two more the next, five lost uploads in total, each with the reason next to it. The next morning's self-check surfaced the same line.

But a machine cannot click "Allow" on a Google sign-in page. The five slots stayed lost until a human sat down.

From the OAuth 2.0 docs, under [refresh token expiration](https://developers.google.com/identity/protocols/oauth2#expiration): a Google Cloud project whose OAuth consent screen is configured for an external user type and has a publishing status of **Testing** is issued a refresh token that expires in seven days.

Read that again if you built your integration the way most tutorials show. You create a project, set up the consent screen, add yourself as a test user, run the local auth flow once, save `token.json`, and schedule the job. Everything in that sequence leaves the app in Testing. The token works on day one. It works on day six. On day seven it does not, and nothing in the auth flow warned you.

Re-authenticating buys you another seven days and nothing else. If that is your fix, you will be doing it every week.

The real fix is changing the publishing status from Testing to **In production**. In my case the "Publish app" button was greyed out, and here is why, in the order I hit it:

Google requires three branding fields before an external app can be published: an application homepage URL, a privacy policy URL, and at least one authorized domain. The authorized domain has to be one you have proven you own, and the proof Google accepts is Search Console verification. My domain was not verified yet, so the branding form would not save, so the publish button would not enable.

So the actual sequence was: open Search Console, add the domain as a property, verify it by uploading an HTML file to the site root, go back to the consent screen, fill in the three branding fields, save, click Publish app, then run the auth flow one more time so the new refresh token is issued under production status. The re-auth itself took about a minute. The rest took the better part of an hour, starting before 5 a.m., because the pipeline had already been down for two days.

After that, `check_youtube_token.js` reports the app as production with no expiry on the token, and the 14:00 slot that afternoon uploaded normally.

One thing to check before you assume you are safe: the token response tells you. When the app is in Testing, the JSON that comes back from the auth flow includes a `refresh_token_expires_in` field counting down from seven days. If you see that field, you are on the clock.

Two things, and neither is clever.

First, the token check runs *before* the expensive part. The runner used to launch Claude Code first and let the upload step, at the very end, discover the dead token. Now the check is the first thing that happens; if it fails, the run costs nothing and the log says exactly why.

Second, "skip with a reason" is treated as a first-class outcome, not a failure to hide. A row that says `SKIP: waiting for YouTube re-auth` is more useful than a row that says `OK` when nothing was uploaded, and a lot more useful than a stack trace at the bottom of a long log. The daily self-check reads those rows and puts the human task at the top of the to-do list.

If you are building anything unattended on Google APIs, the order is: verify the domain, fill in branding, publish the consent screen, *then* run the auth flow, *then* schedule. Doing it in the other order works for exactly one week.

**My take: Google should refuse to issue a refresh token to a Testing app at all, instead of letting it die silently on day seven.** Tell me why I'm wrong — or tell me which of your scheduled jobs is still running on one.

*A Japanese version of this post goes up the same day on [my note.com blog](https://note.com/holy_owl9373).* The runner with the pre-launch token check is [free, pay what you want, on Gumroad](https://tachibana53.gumroad.com/l/claude-code-unattended-runner) — I own the store; that is a plain product link.
