Cache and artifacts look similar and have different guarantees. Cache is a runner-local optimization: it is stored by the runner that created it, and a job scheduled onto a different runner simply starts without it. Nothing fails, the job is just slower or, if it assumed the cache existed, wrong. Artifacts are uploaded to and downloaded from the GitLab instance and are guaranteed to be present for jobs in later stages that declare a dependency.
The frequent mistake is passing compiled output between stages through cache. It works on a single-runner setup and breaks intermittently as soon as a second runner is added, producing a downstream job that cannot find files it expects.
Use cache only for regenerable dependency directories, and set cache:key:files pointing at the lockfile so the key changes when dependencies do. Use artifacts for anything a later job requires. Artifacts default to a 30-day expiry configured per instance, and dependencies: [] on a job skips downloading artifacts it does not need, which is worth setting on long deploy jobs.