Blog · GraphQL
GraphQL Subscriptions with Real-Time Timestamps
Int (Unix seconds) for compact payloads or String (RFC 3339) for human debugging. A custom DateTime scalar gives validation without leaking language-specific Date objects across the wire. Never promise subscription ordering by timestamp alone — add a monotonic sequence or cursor compound key.Choosing Your Timestamp Type in GraphQL Schema
The schema is a contract. If mobile clients already store epoch milliseconds, exposing seconds without renaming fields will cause silent 1000× bugs. Pick one transport unit for all new fields and deprecate the legacy ones with explicit descriptions. For public APIs, ISO strings reduce onboarding friction; for high-rate ticks, ints win bandwidth and parsing cost.
# Unix seconds — compact
type Event {
id: ID!
createdAt: Int!
updatedAt: Int!
}
# Custom scalar — enforce parsing in one place
scalar DateTime
type AuditRow {
id: ID!
recordedAt: DateTime!
}Custom DateTime Scalar Implementation
Apollo Server and GraphQL Yoga both let you coerce values at the boundary. The serialize step must be deterministic for the same instant — do not mix local tz formatting in serialize with UTC in parseValue unless you love bug reports from Australia.
import { GraphQLScalarType, Kind } from 'graphql';
export const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
serialize(value) {
if (value instanceof Date) return Math.floor(value.getTime() / 1000);
if (typeof value === 'number') return value;
throw new TypeError('DateTime cannot represent value');
},
parseValue(value) {
if (typeof value === 'number') return new Date(value * 1000);
if (typeof value === 'string') return new Date(value);
throw new TypeError('DateTime parse error');
},
parseLiteral(ast) {
if (ast.kind === Kind.INT) return new Date(parseInt(ast.value, 10) * 1000);
if (ast.kind === Kind.STRING) return new Date(ast.value);
return null;
},
});Real-Time Subscriptions with Timestamps
Subscriptions multiplex many producers into one consumer socket. Stamp authoritative server time when the event enters the gateway, not when the database driver returns — those two instants diverge under CPU pressure. Emit both publishedAt and seq so UI code can merge partial reconnect buffers deterministically.
type Subscription {
priceQuotes(symbol: String!): PriceTick!
}
type PriceTick {
symbol: String!
price: Float!
asOfSec: Int!
seq: Int!
}
// Resolver sketch — async iterator fan-in
async function* subscribeQuotes(symbol) {
for await (const row of broker.watch(symbol)) {
yield {
priceQuotes: {
...row,
asOfSec: Math.floor(Date.now() / 1000),
seq: nextSeq(symbol),
},
};
}
}Cursor-Based Pagination Using Timestamps
Relay cursors should encode (sort_value, id) tuples, not opaque offsets. When sorting by descending created_at, two rows can share the same millisecond — the tie-breaker prevents pages from dropping rows under load. Base64-JSON cursors are fine; just version them so you can migrate without breaking mobile caches.
Avoiding Common GraphQL Timestamp Mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Float epoch | Client parsers round unpredictably | Use Int seconds or String ISO |
| Trusting client clock | Forged "created" times | Stamp on server only |
| Millis vs seconds drift | Off-by-1000 bugs | Prefix field names with unit |
| Order by time alone | Collisions under burst inserts | Add ULID or bigint id tie-break |
Key takeaways
- Custom scalars centralize coercion — stop repeating parse logic per resolver.
- High-frequency feeds need compact numeric timestamps plus sequence numbers.
- Cursors must be stable under duplicate sort keys.
- Never use native JS Date directly in schema serialization layers.
- Link to knowledge docs when onboarding partners on your time model.
Written by Unix Calculator Editorial Team — Senior Unix/Linux Engineers. Last verified May 2026.
Reference: GraphQL timestamps (knowledge base)
Get the Unix Timestamp Cheatsheet
One email. Instant cheatsheet. No drip sequence.