A transient without an object cache is two rows and a race

Without a persistent object cache a transient is two rows in wp_options — the value and its expiry — and setting one is two writes that are not atomic together.

$data = get_transient( 'turkerdev_report' );

if ( false === $data ) {
    $data = turkerdev_build_report();
    set_transient( 'turkerdev_report', $data, HOUR_IN_SECONDS );
}

// false means "absent", which a legitimately false value cannot be
// distinguished from. store a wrapper:
// set_transient( $k, array( 'v' => $value ), $ttl );

The false ambiguity is the practical trap and a wrapper array is the fix. Expired transients are only cleaned up on a scheduled event, so a site generating many of them accumulates rows indefinitely — and if the transient was autoloaded, those rows are read on every request until something deletes them. Setting a transient with a TTL over a day makes it autoload, which is a detail almost nobody knows.