# Session timeouts with no warning or way to extend

Source: https://easeweb.dev/learn/time-limits
Topics: Time limits and motion > Time limits can be extended (WCAG 2.2.1)
The fix: Either make the session last longer than 20 hours and drop the page's timer, or keep the limit and, two minutes before it ends, open a modal dialog that moves focus to a Stay signed in button and renews the session each time it is pressed. A warning drawn only on screen, which focus is never moved to and a screen reader never reads, is not enough.
Test with: keyboard, screen reader, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/timing-adjustable.html https://www.w3.org/WAI/WCAG22/Techniques/client-side-script/SCR16 https://www.w3.org/WAI/WCAG22/Techniques/client-side-script/SCR1 https://www.w3.org/WAI/WCAG22/Techniques/failures/F40 https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A stationery shop's checkout asks for a delivery address and offers a gift message. The customer is signed in, and the shop signs people out 15 minutes after their last request to the server, a security default copied from a framework's example. Typing into a form sends nothing to the server, so the clock keeps running while someone writes. At 15 minutes the page jumps to the sign-in screen: "Your session has expired." Nothing on the page said there was a limit, nothing warned that it was close, and the address and message are gone. Many sites have only the server half of this: nothing happens on screen, and the form is lost when the customer presses Continue.

Some sites do warn. Two minutes before the end, a banner appears at the top of the screen with a Stay signed in button. It looks like a fix, and it works for a sighted mouse user who notices it. But the banner is added at the end of the page, nothing moves focus to it and nothing reads it out, so a screen reader user hears nothing, a magnifier user zoomed in on the form never sees it, and a keyboard user has to Tab past every field and footer link to reach the button.

## Why it fails

Success Criterion 2.2.1 Timing Adjustable covers every time limit the content sets, and that includes a session the site's own server ends; only limits the content doesn't control, such as a browser's or a network's, are left out. For each one, people must be able to turn it off before they meet it, adjust it to at least ten times the default, or be warned before time runs out and given at least 20 seconds to extend it with a simple action, at least ten times. Three exceptions remain: a real-time event such as an auction, a limit that is essential to the activity, and a limit longer than 20 hours.

The first checkout meets none of these. The second does warn, but only by drawing the warning. 2.2.1 doesn't say how a warning must be given, but a warning has to reach the person it warns: one that a screen reader never reads, that is outside a magnifier's view, and that a keyboard user can only reach after everything else on the page leaves those people with the same deadline and no notice. W3C's technique for this warning, SCR16, uses the browser's own confirm dialog, which every user meets at once.

## Who is affected

How long 15 minutes is depends on how someone fills in a form. A gift message typed by switch, or an address checked against a letter, can easily take longer, and the people who need longer are the ones who lose the most when the form empties: they have to do the slow part twice. On a phone, the on-screen keyboard shrinks the visible part of the page while someone types, and a banner pinned to the top of the screen can end up out of view.

## What automation and AI miss

The limit lives in a script's timer or in the server's session settings, and neither shows in the page a scan reads. The warning, where there is one, appears only after 13 minutes, long after any scan has finished. axe reports the old form of this failure, a `<meta http-equiv="refresh">` that reloads or redirects the page after a delay, which is W3C's failure F40 and F41. The audit also flags any page that shows a countdown or talks about a session ending, for a person to check, but it can't see a limit that the page never mentions. AI assistants asked for "auto logout after inactivity" often write exactly the first checkout's timer, and when asked for a warning, add a toast or banner in a corner that is never announced. Only someone who waits it out, with a keyboard, a screen reader or a magnifier, sees the failure.

## Before: a session that signs out with no warning

```js server.js
// A session ends 15 minutes after the last request.
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
```

```js
// Sent to the sign-in screen at the same moment.
const signOut = () => { location.href = '/sign-in?expired=1'; };
setTimeout(signOut, 15 * 60 * 1000);
```

The page signs people out 15 minutes after it loaded, whatever they are doing, and nothing lets them turn the limit off, lengthen it or ask for more time. Everything typed into the form is lost with the page.

## Before: a warning only the screen shows

```js server.js
// A session ends 15 minutes after the last request.
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
```

```js
const LIMIT = 15 * 60 * 1000;
const WARN = 2 * 60 * 1000;
let warnTimer, endTimer;

function start() {
  clearTimeout(warnTimer);
  clearTimeout(endTimer);
  warnTimer = setTimeout(warn, LIMIT - WARN);
  endTimer = setTimeout(() => { location.href = '/sign-in?expired=1'; }, LIMIT);
}

// A banner pinned to the top of the screen, added at the end of the page.
function warn() {
  const banner = document.createElement('div');
  banner.className = 'session-warning';
  banner.innerHTML = 'You will be signed out in 2 minutes. <button type="button">Stay signed in</button>';
  banner.querySelector('button').addEventListener('click', async () => {
    await fetch('/session/extend', { method: 'POST' });
    banner.remove();
    start();
  });
  document.body.append(banner);
}
start();
```

```css
.session-warning { position: fixed; inset: 0 0 auto; padding: 0.75rem 1rem; background: #fff4d6; }
```

The warning and the button are right, and a mouse user who sees the banner can stay. But focus stays in the form, nothing is announced, and the banner sits after the footer in the page, so screen reader users are never told, magnifier users don't see it, and keyboard users reach it last.

## After: a warning dialog that keeps people signed in

```html
<dialog id="session-warning" role="alertdialog"
  aria-labelledby="session-title" aria-describedby="session-text">
  <h2 id="session-title">Do you need more time?</h2>
  <p id="session-text">For your security, you will be signed out in 2 minutes. Anything you have entered will be lost.</p>
  <form method="dialog">
    <button autofocus>Stay signed in</button>
  </form>
</dialog>
```

```js server.js
// The same 15-minute session, renewed by any request.
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 15 * 60 * 1000 } }));
app.post('/session/extend', (req, res) => res.sendStatus(204));
```

```js
const LIMIT = 15 * 60 * 1000;
const WARN = 2 * 60 * 1000;
const dialog = document.getElementById('session-warning');
let warnTimer, endTimer;

function start() {
  clearTimeout(warnTimer);
  clearTimeout(endTimer);
  warnTimer = setTimeout(() => dialog.showModal(), LIMIT - WARN);
  endTimer = setTimeout(() => { location.href = '/sign-in?expired=1'; }, LIMIT);
}

// Stay signed in, or Escape: renew the session, as often as people need.
dialog.addEventListener('close', async () => {
  await fetch('/session/extend', { method: 'POST' });
  start();
});
start();
```

The limit stays, but nobody meets it unawares. Two minutes before the end, `showModal()` opens the warning over the page and moves focus to Stay signed in, so a screen reader can read the title, the time left and the button, a magnifier follows focus to it, and a keyboard user is already on it. One press, or Escape, renews the session on the server and starts the page's clock again, as many times as people need, and the browser puts focus back where it was, so people carry on typing. If nobody answers, the session ends as before, which is what the limit is for on a shared computer.

## After: a session that lasts longer than 20 hours

```js server.js
// A session lasts 30 days from the last visit.
app.use(session({ secret: process.env.SESSION_SECRET, rolling: true, cookie: { maxAge: 30 * 24 * 60 * 60 * 1000 } }));
```

```js
// No timer: nothing on the page signs anyone out.
```

2.2.1 exempts a limit longer than 20 hours, so nobody filling in the form meets one, and there is nothing to warn about and no dialog to build. This is the smallest change: one number on the server, and one timer deleted from the page. It suits a site where a session left open protects little, such as a shop that already asks for the password again before an account's email address, password or saved cards can be changed. It does not suit a bank, a health record, or anything used on shared computers, where the limit is there for a reason.

## Which option to use

Both pass 2.2.1, and which one fits depends on what the session protects, not on accessibility. If the 15 minutes came from a default nobody chose, make the session longer than 20 hours: no one is interrupted, and nothing needs building. If the limit is there for security, keep it and add the warning dialog, timed for your own limit; it costs a dialog, a few lines of script and an endpoint, and it keeps the protection for people who really have walked away. Never keep the banner that is only drawn on screen.

## Implementation decisions

**Turning the limit off before people meet it also passes.** A "Give me more time" box at the start of a long form, or a "Keep me signed in" box at sign-in that makes the session longer than 20 hours, lets people turn the limit off, or set one at least ten times as long, before they meet it, which 2.2.1 accepts. It only helps people who notice it and understand what it does, so on a security-bound site keep the warning as well.

**How long to warn.** 20 seconds is the minimum, chosen so that almost everyone can press a single key or switch. Reading a dialog with a screen reader, or noticing it after looking away, takes longer; two minutes costs nothing and covers far more people. Count the warning from the server's limit, and if the page itself makes requests (an address lookup, a saved draft), restart the page's clock on each one, so the two agree.

**The time left.** Give the time in the dialog's text, as the fix does. A clock that counts down every second can be drawn as well, but give it `aria-hidden="true"` and keep it out of any live region, or a screen reader reads every second over the button.

**Count from what people do.** The fix counts from the last request, as the server does. A site that wants active people never to see the dialog can also renew the session while they type, for example with one request a minute while input events arrive, and keep the dialog for when they stop.

**Asleep and in other tabs.** A laptop that sleeps through the limit wakes up past it with no warning ever shown. Timers don't run while it sleeps, so whenever one fires, and when the page is shown again (`visibilitychange`), compare `Date.now()` with when the limit started; once it has passed, ask the server whether the session is still there, and if it isn't, say so on the page and keep what was typed. When people have several tabs open, renew the session from whichever tab answers, and close the dialog in the others, for example through a `BroadcastChannel`.

**Keep what was typed.** Even with a warning, someone who misses it loses the form. Saving a draft, in the browser or on the server, and putting it back after people sign in again goes beyond 2.2.1: it is what 2.2.5 Re-authenticating asks for, a topic of its own.

**Timed holds.** A countdown on seats or stock held during checkout is a time limit too. W3C's own examples treat a two-minute hold on tickets that can sell out quickly as essential, and move as much of the process as possible, such as entering names and payment details, out of the timed part; where the hold can be extended, the same warning dialog with an "Extend time" button passes. Showing the countdown is not adjusting it: a clock in the corner with no button is still a limit nobody can change.

**Not a fix.** `role="alert"` on a banner that is in the page before its message is written into it gets the message read out, but focus stays in the form and the button stays at the end of the page. A longer fixed limit, such as 60 minutes instead of 15, is still a limit someone will meet without warning.

## Verify the fix

1. Read the server's session settings and the page's scripts, and list every limit: session lifetimes, timers that sign out, clear or redirect, and any `<meta http-equiv="refresh">` with a delay. Each is longer than 20 hours, can be turned off, or has a warning before it.
2. In a test build with the limit shortened to a few minutes, fill in the form slowly and then leave it alone: either nothing happens, or a warning appears at least 20 seconds before the end, and if nobody answers, the session ends.
3. Using only the keyboard, let the warning open: focus is on Stay signed in, and Enter keeps you signed in with the form as you left it. Do it ten times in a row, and it still works.
4. With a screen reader running in the form, let the warning open: its title, the time left and the focused button are read out without you moving.
5. With a screen magnifier zoomed in on the gift message, let the warning open: it comes into view without searching for it.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the session settings, the sign-in flow or the warning change.

## Limits

This covers time limits that end a session or a page, and one way of warning about them. Content that moves, scrolls or updates by itself has its own criterion and guide: [tickers, carousels and animations that can't be paused](/learn/moving-content). A message that disappears on a timer is a 2.2.1 limit too, with its own guide: [toasts that vanish before you can read or undo them](/learn/toast-timing). A page that reloads or redirects itself on a timer, as a delayed meta refresh does, is a topic of its own. Telling people how long they can be idle before they lose data, and designing a process with no timing at all, are AAA topics of their own. Whether a limit is really essential or part of a real-time event is a judgement only the people who run the service can make, and screen readers handle dialogs that open by themselves differently: test the warning with the ones your users have.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
