The Problem: Why clock() Doesn't Measure Real-World Elapsed Time

If you are building a reaction time tester or a stopwatch in C, you might be tempted to use the standard clock() function from <time.h> to measure how long a user takes to respond. However, you will likely notice that your measured reaction time is always a tiny, nearly identical number (such as 25–40 ticks), regardless of whether the user waited 100 milliseconds or 10 seconds.

This happens because of a fundamental misunderstanding of what clock() actually measures: CPU execution time, not wall-clock (real-world) time.

Understanding CPU Time vs. Wall-Clock Time

  • CPU Time (Process Time): The amount of time the CPU actively spends executing instructions for your specific program. When your program halts at getchar() or scanf() waiting for user input, the operating system puts the process into a waiting/sleep state. Because the CPU is not executing code during this wait, clock() does not advance.
  • Wall-Clock Time (Real Time): The actual time that passes in the physical world (like the clock on your wall). This is what you need to measure human reaction speeds.
  • Busy Waiting (CPU Burn): In contrast, if you use a busy-wait loop like while (time(NULL) < target);, the CPU is running at 100% usage during that delay, which wastes CPU cycles and distorts process-time metrics.

The Modern, Cross-Platform Solution: timespec_get()

Starting in C11, C introduced timespec_get() into the standard <time.h> library. It works natively across modern compilers on both Linux (GCC/Clang) and Windows (MSVC/MinGW) without requiring platform-specific headers like <windows.h> or <sys/time.h>.

timespec_get() provides nanosecond-level wall-clock precision, making it ideal for games, input timers, and reaction testers.

Complete Working Reaction Timer Code

Here is a complete, clean implementation of a reaction timer that properly measures real elapsed time in milliseconds and sleeps without maxing out CPU cores:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#ifdef _WIN32
    #include <windows.h>
    #define sleep_ms(ms) Sleep(ms)
#else
    #include <unistd.h>
    #define sleep_ms(ms) usleep((ms) * 1000)
#endif

// Helper to get current wall-clock time in milliseconds
double get_wall_time_ms(void) {
    struct timespec ts;
    timespec_get(&ts, TIME_UTC);
    return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1000000.0;
}

// Clear standard input buffer
void clear_input_buffer(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF);
}

int main(void) {
    srand((unsigned int)time(NULL));

    printf("=== REACTION SPEED TESTER ===\n");
    printf("When you see 'START', press Enter as quickly as possible.\n");
    printf("The test will repeat 5 times.\n");
    printf("Press Enter to begin...");
    getchar();

    double total_reaction_time = 0.0;

    for (int i = 1; i <= 5; i++) {
        int wait_seconds = (rand() % 5) + 1;
        printf("\nRound %d: Waiting...\n", i);
        
        // Non-busy sleep
        sleep_ms(wait_seconds * 1000);

        printf(">>> START! <<<\n");
        double start_time = get_wall_time_ms();

        // Wait for user to press Enter
        clear_input_buffer();

        double end_time = get_wall_time_ms();
        double reaction_time = end_time - start_time;

        printf("Reaction time: %.2f ms\n", reaction_time);
        total_reaction_time += reaction_time;
    }

    printf("\n==============================\n");
    printf("Average Reaction Time: %.2f ms\n", total_reaction_time / 5.0);
    printf("==============================\n");

    return 0;
}

Key Takeaways & Improvements

  • Never use clock() for I/O waits: Always use wall-clock functions like timespec_get() or POSIX clock_gettime(CLOCK_MONOTONIC, ...) when timing external events or user interaction.
  • Avoid busy-waiting: Avoid empty while loops to create delays. Use operating-system level sleep functions (Sleep on Windows, usleep/nanosleep on Unix) to release CPU resources.
  • Handle Input Buffers: When reading keys using getchar(), stray newline characters (\n) can remain in the buffer. Consuming the buffer properly guarantees the test only measures the user's actual keystroke after the prompt appears.