One way to cache assets is with ETags. When an asset is requested for the first time, the response includes the ETag header. On subsequent requests, the browser sends the ETag back to the server via the If-None-Match header. The server then compares the ETag it received from the browser to the current version of the ETag. If the ETag matches, it responds with a 304 Not Modified and an empty body which saves drastically on bandwidth.
The Cache-Control header allows the server to give more complex instructions to the browser about caching behavior. When max-age is specified, the server can tell the browser to keep that asset and not ask about it again for a specified number of seconds. For example, Cache-Control: max-age=3600 tells the browser that the response should be cached for 1 hour. That not only cuts down on bandwidth but also HTTP requests.
There is a danger is telling the browser to cache and not validate whether a resource has been updated recently. If you update the HTML structure of your page but your CSS is being cached for an hour, then your site could look broken until the browser checks to see whether the CSS has been updated.
Fingerprinting solves that problem. Instead of referring to mysite.css in the HTML, we calculate a fingerprint (md5 hash) of mysite.css and add it to the file name so that now we have mysite-29fe7a48516763c7c3033929e3d4c2a0.css. The fingerprint will change when the CSS changes which changes the filename. Now we know these files will never go stale and therefore can be cached forever. We can set max-age to one year which is the maximum value for max-age. Now when we update our HTML and CSS, the filename of the CSS changes so the browser asks for the new file.
You don't want to calculate these fingerprints on your own and update your HTML by hand to refer to the new versions. You want a framework or build process which does that automagically for you. We use node so we found asset-rack. In our HTML templates we just call assets.url('/mysite.css') and it handles all the magic of fingerprinting for us.
There was a little heartburn during development at first because we had to restart the app everytime our CSS changed so that asset-rack could update it's own cache (ironically). Nodemon solved that problem for us. We configure it to watch all CSS and JS files and automatically restart the app when anything changes: nodemon -e js,css app.js.
Now we don't have to think about caching.