diff --git a/test/validation/odp_timer.c b/test/validation/odp_timer.c
index 4c93f71..d893646 100644
--- a/test/validation/odp_timer.c
+++ b/test/validation/odp_timer.c
@@ -8,10 +8,13 @@
  * @file
  */
 
-#include <assert.h>
+/* For rand_r and nanosleep */
+#define _POSIX_C_SOURCE 200112L
+#include <time.h>
 #include <unistd.h>
 #include <odp.h>
 #include "odp_cunit_common.h"
+#include "test_debug.h"
 
 /** @private Timeout range in milliseconds (ms) */
 #define RANGE_MS 2000
@@ -28,6 +31,9 @@ static odp_buffer_pool_t tbp;
 /** @private Timer pool handle used by all threads */
 static odp_timer_pool_t tp;
 
+/** @private Count of timeouts delivered too late */
+static odp_atomic_u32_t ndelivtoolate;
+
 /** @private min() function */
 static int min(int a, int b)
 {
@@ -47,8 +53,7 @@ struct test_timer {
 /* @private Handle a received (timeout) buffer */
 static void handle_tmo(odp_buffer_t buf, bool stale, uint64_t prev_tick)
 {
-	/* Use assert() for internal correctness checks of test program */
-	assert(buf != ODP_BUFFER_INVALID);
+	CU_ASSERT_FATAL(buf != ODP_BUFFER_INVALID); /* Internal error */
 	if (odp_buffer_type(buf) != ODP_BUFFER_TYPE_TIMEOUT) {
 		/* Not a timeout buffer */
 		CU_FAIL("Unexpected buffer type received");
@@ -65,38 +70,41 @@ static void handle_tmo(odp_buffer_t buf, bool stale, uint64_t prev_tick)
 	if (ttp == NULL)
 		CU_FAIL("odp_timeout_user_ptr() null user ptr");
 
-	if (ttp->buf2 != buf)
+	if (ttp != NULL && ttp->buf2 != buf)
 		CU_FAIL("odp_timeout_user_ptr() wrong user ptr");
-	if (ttp->tim != tim)
+	if (ttp != NULL && ttp->tim != tim)
 		CU_FAIL("odp_timeout_timer() wrong timer");
 	if (stale) {
 		if (odp_timeout_fresh(tmo))
 			CU_FAIL("Wrong status (fresh) for stale timeout");
 		/* Stale timeout => local timer must have invalid tick */
-		if (ttp->tick != TICK_INVALID)
+		if (ttp != NULL && ttp->tick != TICK_INVALID)
 			CU_FAIL("Stale timeout for active timer");
 	} else {
 		if (!odp_timeout_fresh(tmo))
 			CU_FAIL("Wrong status (stale) for fresh timeout");
 		/* Fresh timeout => local timer must have matching tick */
-		if (ttp->tick != tick) {
-			printf("Wrong tick: expected %"PRIu64" actual %"PRIu64"\n",
-			       ttp->tick, tick);
+		if (ttp != NULL && ttp->tick != tick) {
+			LOG_DBG("Wrong tick: expected %"PRIu64" actual %"PRIu64"\n",
+				ttp->tick, tick);
 			CU_FAIL("odp_timeout_tick() wrong tick");
 		}
 		/* Check that timeout was delivered 'timely' */
 		if (tick > odp_timer_current_tick(tp))
 			CU_FAIL("Timeout delivered early");
 		if (tick < prev_tick) {
-			printf("Too late tick: %"PRIu64" prev_tick %"PRIu64"\n",
-			       tick, prev_tick);
-			CU_FAIL("Timeout delivered late");
+			LOG_DBG("Too late tick: %"PRIu64" prev_tick %"PRIu64"\n",
+				tick, prev_tick);
+			/* We don't report late timeouts using CU_FAIL */
+			odp_atomic_inc_u32(&ndelivtoolate);
 		}
 	}
 
-	/* Use assert() for correctness check of test program itself */
-	assert(ttp->buf == ODP_BUFFER_INVALID);
-	ttp->buf = buf;
+	if (ttp != NULL) {
+		/* Internal error */
+		CU_ASSERT_FATAL(ttp->buf == ODP_BUFFER_INVALID);
+		ttp->buf = buf;
+	}
 }
 
 /* @private Worker thread entrypoint which performs timer alloc/set/cancel/free
@@ -203,8 +211,11 @@ static void *worker_entrypoint(void *arg)
 			/* Save expected expiration tick */
 			tt[i].tick = cur_tick + tck;
 		}
-		if (usleep(1000/*1ms*/) < 0)
-			perror("usleep"), abort();
+		struct timespec ts;
+		ts.tv_sec = 0;
+		ts.tv_nsec = 1000000; /* 1ms */
+		if (nanosleep(&ts, NULL) < 0)
+			perror("nanosleep"), abort();
 	}
 
 	/* Cancel and free all timers */
@@ -220,18 +231,22 @@ static void *worker_entrypoint(void *arg)
 			CU_FAIL("odp_timer_free");
 	}
 
-	printf("Thread %u: %u timers set\n", thr, nset);
-	printf("Thread %u: %u timers reset\n", thr, nreset);
-	printf("Thread %u: %u timers cancelled\n", thr, ncancel);
-	printf("Thread %u: %u timers reset/cancelled too late\n",
-	       thr, ntoolate);
-	printf("Thread %u: %u timeouts received\n", thr, nrcv);
-	printf("Thread %u: %u stale timeout(s) after odp_timer_free()\n",
-	       thr, nstale);
+	LOG_DBG("Thread %u: %u timers set\n", thr, nset);
+	LOG_DBG("Thread %u: %u timers reset\n", thr, nreset);
+	LOG_DBG("Thread %u: %u timers cancelled\n", thr, ncancel);
+	LOG_DBG("Thread %u: %u timers reset/cancelled too late\n",
+		thr, ntoolate);
+	LOG_DBG("Thread %u: %u timeouts received\n", thr, nrcv);
+	LOG_DBG("Thread %u: %u stale timeout(s) after odp_timer_free()\n",
+		thr, nstale);
 
 	/* Delay some more to ensure timeouts for expired timers can be
 	 * received */
-	usleep(1000/*1ms*/);
+	struct timespec ts;
+	ts.tv_sec = 0;
+	ts.tv_nsec = 1000000; /* 1ms */
+	if (nanosleep(&ts, NULL) < 0)
+		perror("nanosleep"), abort();
 	while (nstale != 0) {
 		odp_buffer_t buf = odp_queue_deq(queue);
 		if (buf != ODP_BUFFER_INVALID) {
@@ -247,7 +262,7 @@ static void *worker_entrypoint(void *arg)
 	if (buf != ODP_BUFFER_INVALID)
 		CU_FAIL("Unexpected buffer received");
 
-	printf("Thread %u: exiting\n", thr);
+	LOG_DBG("Thread %u: exiting\n", thr);
 	return NULL;
 }
 
@@ -256,9 +271,13 @@ static void test_odp_timer_all(void)
 {
 	odp_buffer_pool_param_t params;
 	odp_timer_pool_param_t tparam;
-	/* This is a stressfull test - need to reserve some cpu cycles
-	 * @TODO move to test/performance */
-	int num_workers = min(odp_sys_cpu_count()-1, MAX_WORKERS);
+	/* Reserve at least one core for running other processes so the timer
+	 * test hopefully can run undisturbed and thus get better timing
+	 * results. */
+	int num_workers = min(odp_sys_cpu_count() - 1, MAX_WORKERS);
+	/* On a single-CPU machine run at least one thread */
+	if (num_workers < 1)
+		num_workers = 1;
 
 	/* Create timeout buffer pools */
 	params.buf_size  = 0;
@@ -294,19 +313,11 @@ static void test_odp_timer_all(void)
 	CU_ASSERT(tpinfo.param.res_ns == RES);
 	CU_ASSERT(tpinfo.param.min_tmo == MIN);
 	CU_ASSERT(tpinfo.param.max_tmo == MAX);
-	printf("Timer pool\n");
-	printf("----------\n");
-	printf("  name: %s\n", tpinfo.name);
-	printf("  resolution: %"PRIu64" ns (%"PRIu64" us)\n",
-	       tpinfo.param.res_ns, tpinfo.param.res_ns / 1000);
-	printf("  min tmo: %"PRIu64" ns\n", tpinfo.param.min_tmo);
-	printf("  max tmo: %"PRIu64" ns\n", tpinfo.param.max_tmo);
-	printf("\n");
-
-	printf("#timers..: %u\n", NTIMERS);
-	printf("Tmo range: %u ms (%"PRIu64" ticks)\n", RANGE_MS,
-	       odp_timer_ns_to_tick(tp, 1000000ULL * RANGE_MS));
-	printf("\n");
+	CU_ASSERT(strcmp(tpinfo.name, NAME) == 0);
+
+	LOG_DBG("#timers..: %u\n", NTIMERS);
+	LOG_DBG("Tmo range: %u ms (%"PRIu64" ticks)\n", RANGE_MS,
+		odp_timer_ns_to_tick(tp, 1000000ULL * RANGE_MS));
 
 	uint64_t tick;
 	for (tick = 0; tick < 1000000000000ULL; tick += 1000000ULL) {
@@ -319,6 +330,9 @@ static void test_odp_timer_all(void)
 	/* Initialize barrier used by worker threads for synchronization */
 	odp_barrier_init(&test_barrier, num_workers);
 
+	/* Initialize the shared timeout counter */
+	odp_atomic_init_u32(&ndelivtoolate, 0);
+
 	/* Create and start worker threads */
 	pthrd_arg thrdarg;
 	thrdarg.testcase = 0;
@@ -327,6 +341,8 @@ static void test_odp_timer_all(void)
 
 	/* Wait for worker threads to exit */
 	odp_cunit_thread_exit(&thrdarg);
+	LOG_DBG("Number of timeouts delivered/received too late: %u\n",
+		odp_atomic_load_u32(&ndelivtoolate));
 
 	/* Check some statistics after the test */
 	if (odp_timer_pool_info(tp, &tpinfo) != 0)
