The Fundamental Problem
Every user's device has a clock, and that clock might be wrong. Users may have incorrect system time, a different timezone than expected, or — in adversarial scenarios — deliberately manipulate their clock to game time-based features like flash sales, session timeouts, or free trial periods. The core question: which clock do you trust for what purpose?
Use Server Time For:
- Record creation timestamps: When was this order placed? When was this account created? Always use server UTC.
- Authorization decisions: Has this session expired? Is this coupon still valid? Is the flash sale active? Server time only.
- Audit logs: Any time you need an immutable, trusted record of when something happened.
- Rate limiting: How many requests has this user made in the last 60 seconds?
- Scheduled jobs: Cron jobs, task queues, email scheduling — all server-side.
Use Client Time For:
- Local display: Showing the user their local time, date pickers in their timezone.
- Low-stakes UI features: A "relative time" display ("2 minutes ago") where slight inaccuracy doesn't matter.
- Timezone detection: Reading
Intl.DateTimeFormat().resolvedOptions().timeZoneto pre-fill timezone preferences. - Performance timing: Measuring how long a user interaction takes on their device.
Never Trust the Client For:
# BAD: Using client-supplied timestamp for record creation
@app.route("/purchase", methods=["POST"])
def purchase():
timestamp = request.json["timestamp"] # NEVER trust this!
order = Order(created_at=timestamp) # An attacker can set any value
# GOOD: Generate timestamp on the server
@app.route("/purchase", methods=["POST"])
def purchase():
order = Order(created_at=datetime.now(timezone.utc))
Handling Clock Skew Between Server and Client
A common pattern is to include the server's current timestamp in API responses, allowing clients to calculate the offset between server and client time:
// Server response includes server_time
const { data, server_time } = await fetch("/api/status").then(r => r.json());
const skew = Date.now() - new Date(server_time).getTime();
// Adjust client-side countdowns using the skew
function serverNow() {
return Date.now() - skew;
}
Flash Sales and Time-Based Features
For features like flash sales where timing matters: render the sale's start/end time from the server in UTC, and do all validation server-side. On the client, display a countdown using performance.now() for accuracy, but always re-validate on every server request.
# Server: always validate timing server-side
def process_flash_sale_order(sale_id, user_id):
sale = Sale.objects.get(id=sale_id)
now = datetime.now(timezone.utc)
if not (sale.starts_at <= now <= sale.ends_at):
raise ValidationError("Sale is not currently active")