Skip to content
IntermediateESP32esp32-camtimelapsesd-card

ESP32-CAM Timelapse Recorder — Interval Photos to SD Card

Program ESP32-CAM to capture JPEG frames at a configurable interval and save them to a microSD card. Includes PSRAM configuration, file naming with frame counter, and a Python assembly script to create the final MP4 timelapse.

SolderHub10/17/2026 16 min read 0 views

ESP32-CAM Timelapse Recorder — Interval Photos to SD Card

Why Build This Instead of Just Using a Phone

The honest answer is that I wanted to point a camera at a 3D print for eleven hours and not think about it again until it was done. A phone works for the first twenty minutes of any timelapse — then it locks, or a notification interrupts the camera app, or the battery gives out at hour six and you're left with half a timelapse and a lesson about propping your phone against something more reliable. An ESP32-CAM on a 5V wall supply doesn't care how long the shoot runs. Plug it in, walk away, come back when it's done.

The AI Thinker ESP32-CAM is a strange little board — genuinely capable camera hardware bolted onto a microcontroller that's also expected to write files, manage a WiFi radio it isn't even using for this project, and do it all without the benefit of an operating system holding its hand. Getting a clean, well-exposed frame onto an SD card at a controlled interval, indefinitely, without a laptop attached, touches almost every constraint the board has: PSRAM, SD_MMC bus sharing, deep sleep timing, and the surprisingly fiddly business of getting the sensor to actually expose properly before you grab the frame you're going to keep. This build walks through all of it, plus the Python script that turns a folder of ten thousand tiny JPEGs into an actual video file at the end.

Animated preview of a finished 11-hour 3D print timelapse, captured with this exact build
ESP32-CAM mounted on a small tripod, framed on a 3D printer bed for an overnight timelapse

Why PSRAM Matters for Timelapse

The AI Thinker ESP32-CAM has 4 MB of PSRAM (Pseudo-Static RAM) connected via the SPI bus, separate from the small amount of internal SRAM built into the ESP32 chip itself. Without PSRAM enabled, the camera driver has nowhere to put a large frame buffer, so you're capped at FRAMESIZE_CIF — 352×288 — which is fine for a motion-detection preview and genuinely bad for anything you intend to watch as a finished video. With PSRAM enabled, the same board captures UXGA (1600×1200) frames without complaint, which is enough resolution for a timelapse that actually looks intentional rather than like a security camera clip from 2009.

This is also the single most common reason someone's ESP32-CAM sketch compiles fine, uploads fine, and then either crashes on the first capture or silently falls back to a resolution nobody asked for: PSRAM has to be explicitly enabled in the Arduino IDE board settings, and it's not the default. If psramFound() returns false when you'd swear the board has PSRAM, that setting — not the hardware — is almost always the actual problem.

The other constraint worth understanding up front: the microSD card and the camera share physical bus lines on this board (it's called SD_MMC mode, a 1-bit variant of the SD protocol repurposed to share pins with the camera interface). That means capture and file-write can't genuinely happen in parallel — the sequence is always capture frame into the buffer, then write that buffer to the card, then release the buffer, one step after another. It's not a bottleneck for a timelapse shooting a frame every 30 seconds or slower, but it's worth knowing about before assuming you can push this board to fast burst-capture rates without redesigning the whole capture loop around it.

Arduino IDE Tools menu with PSRAM set to "OPI PSRAM" -- the setting most often missed before a first capture

Parts List

ComponentNotes
ESP32-CAM (AI Thinker)The most common variant; other ESP32-CAM boards use different pin mappings
microSD card, 32 GB, Class 10 or U1Must be formatted FAT32 — the SD_MMC library doesn't support exFAT
FTDI USB-to-serial adapter (3.3V)The ESP32-CAM has no onboard USB — this is how you flash it
5V 2A power supplyThe camera module briefly draws a real current spike on capture; USB-from-a-laptop power is often not quite enough for reliable long runs
Mini tripod or camera mountWhatever keeps the lens pointed at the same spot for the whole shoot
Full parts list laid out before assembly -- ESP32-CAM, FTDI adapter, microSD card, and 5V supply

Wiring for Programming

The ESP32-CAM doesn't have a USB port, which catches people off guard the first time they unbox one. Flashing it means wiring an FTDI adapter to it temporarily:

ESP32-CAM PinFTDI Pin
5VVCC (5V, not 3.3V — the board regulates internally)
GNDGND
U0R (RX)TX
U0T (TX)RX
GPIO 0GND (only during upload — see below)

GPIO 0 to GND is the part everyone forgets. The ESP32-CAM only enters programming mode if GPIO 0 is grounded at the moment it powers on or resets. Wire a jumper from GPIO 0 to GND, power-cycle the board (or press its reset button if it has one — many breakout boards do), start the upload, and once you see "Connecting..." in the Arduino IDE output, the board is listening. After the upload finishes, remove the GPIO 0 jumper and power-cycle again — with it still grounded, the board boots back into programming mode instead of running the sketch, which looks identical to "the code isn't running" if you don't know to check for it.

FTDI-to-ESP32-CAM wiring for flashing, with the GPIO 0 jumper to GND visible for programming mode

The Code

C++

Two design choices in here are worth calling out because they're easy to get wrong on a first attempt. First, this uses deep sleep between captures instead of a delay() loop, and that's deliberate — a delay(30000) sitting in loop() keeps the camera sensor, the WiFi radio, and the full CPU powered the entire time, which is wasteful for a board that's going to sit plugged into a wall for eleven hours anyway, and it also means any crash or memory leak in a long-running loop compounds over the whole shoot. Deep sleep resets the board completely between frames — setup() runs fresh every single time, camera and SD card get reinitialized from a clean state, and frameCounter survives across the sleep because it's declared with RTC_DATA_ATTR, which places it in the small slice of RAM that deep sleep doesn't wipe. The tradeoff is that a lot of the code you might expect to write once in setup() and never touch again — camera init, SD mount — runs on every single frame instead. For a 30-second interval that overhead is a non-issue; it would matter more if you tried to push this toward one-second intervals.

Second, the first frame after camera init gets thrown away. Camera sensors need a handful of frames to settle their auto-exposure and auto-white-balance algorithms after waking from a cold start, and the very first frame out of a freshly initialized sensor is reliably darker or color-shifted compared to everything after it. Discarding that first frame and grabbing a second one after a short delay is a small amount of extra code that fixes a problem you'd otherwise only notice by scrubbing through the finished timelapse and seeing every single frame flicker slightly at a regular interval — which, once you know to look for it, is an obvious and slightly embarrassing artifact to leave in a finished video.

Serial monitor output showing successive frame captures written to the SD card, one per wake cycle

Choosing an Interval

The interval and the eventual playback frame rate together decide how much real time one second of finished video represents. At 30-second intervals played back at 24 FPS, each second of video covers 12 minutes of real time — an eleven-hour print finishes as a roughly 55-second clip, which is close to the sweet spot for something people will actually watch instead of losing patience with by the 20-second mark.

SubjectSuggested IntervalWhy
3D print (multi-hour)20–40 secEnough motion between frames to read as continuous without the file count becoming unmanageable
Cloud/sky timelapse5–15 secClouds move fast enough that longer intervals produce visible judder
Plant growth over days5–15 minGrowth is slow enough that shorter intervals just waste storage
Construction/long-term project15–60 minDays-to-weeks timescale; storage budget usually the limiting factor, not motion smoothness

A 32 GB card at UXGA resolution and quality 10 holds roughly 6,000–8,000 frames depending on scene complexity, which at a 30-second interval is around 50–65 hours of continuous shooting before the card fills. Longer shoots either need a bigger card, a coarser interval, or a scheduled offload — this sketch doesn't delete old frames automatically, on purpose, since silently losing footage partway through a shoot is a worse failure mode than the card just filling up and the board stopping cleanly.

Side-by-side animated comparison of the same print timelapsed at a 20-second interval versus a 60-second interval

Assembling the Final Video

Once the shoot is done, pull the SD card, copy the /timelapse folder to a computer, and run this against it:

Python
Terminal output from assemble_timelapse.py, showing FFmpeg stitching the captured frames into the final MP4

The -pattern_type glob flag is what lets FFmpeg consume the %05lu.jpg filenames the sketch writes without needing them renumbered first — FFmpeg's default image-sequence input wants a strict %05d pattern starting from a specific number, and glob mode sidesteps that by just sorting whatever matches the wildcard alphabetically, which works cleanly since zero-padded five-digit filenames sort correctly as strings. -crf 18 sits close to visually lossless for x264 — going lower gives diminishing returns in quality for a large jump in file size, and going much higher starts introducing visible blocking in high-contrast footage like a 3D printer's illuminated build plate.

Getting the Shot Actually in Focus

The camera module on most AI Thinker boards is a fixed-focus OV2640 with a manually adjustable lens ring — a small threaded barrel you twist to nudge focus, not an autofocus system. Nobody mentions this until after the first ruined shoot, which was exactly my experience: I set up an eleven-hour print timelapse, checked that the Serial Monitor was happily printing "Saved: /timelapse/00001.jpg" every 30 seconds, and walked away satisfied. Every single one of those frames turned out soft enough to be unusable, because the lens had shipped focused for roughly a meter out and I'd mounted the board about fifteen centimeters from the print bed.

The fix is unglamorous: before starting a real shoot, run a short test capture, pull a frame off the card, and actually look at it at full resolution — not the tiny thumbnail your file browser shows by default. If it's soft, gently rotate the lens ring in small increments, capture again, and repeat until a test frame at the actual working distance comes back sharp. It takes five minutes and it's the difference between a timelapse worth watching and eleven hours of footage you have to throw away. Once it's focused correctly for a given mounting distance, it holds — this isn't something that drifts on its own, so it only needs doing once per physical setup, not before every shoot.

Framing matters more than it seems like it should for something as forgiving as a timelapse. A slightly wider shot than feels necessary gives some headroom if the mount shifts a millimeter or two over a multi-hour run — vibration from a nearby 3D printer's stepper motors is enough to very slowly walk a lightweight mount out of position over several hours, and a tighter frame makes that drift obvious and distracting in the finished video where a wider frame would have absorbed it without anyone noticing.

Side-by-side comparison of an out-of-focus test frame and the same shot after adjusting the lens ring

A Note on Power

The datasheet current draw for this board during capture is higher than people expect from something this small — the camera sensor, the flash LED if it's enabled, and a brief burst of SD write activity can spike well past what the module draws at idle. It's rarely enough to cause a problem on a proper 5V 2A supply, but it's exactly the kind of load that exposes a marginal USB cable or a cheap phone charger that can't hold its rated voltage under a transient spike. If a shoot mysteriously stops a few hours in with no error printed to Serial — because nothing was connected to Serial to catch it — a brownout reset from an underpowered supply is the first thing worth ruling out, ahead of anything in the code. Swapping to a supply and cable you know are rated for a full 2A, not just labeled that way, resolves more of these "randomly stopped overnight" reports than any firmware change does.

Dedicated 5V 2A wall supply and short, thick USB cable used for a reliable multi-hour unattended shoot

Setup Steps

  1. Format the SD card as FAT32 — exFAT is not supported by the SD_MMC library and will fail silently or refuse to mount.
  2. In Arduino IDE, go to Tools → PSRAM and select "OPI PSRAM." Skipping this step is the most common reason UXGA capture fails on hardware that objectively has the PSRAM chip present.
  3. Wire the FTDI adapter with GPIO 0 shorted to GND, then upload.
  4. Remove the GPIO 0 jumper and power-cycle the board. The onboard flash LED should pulse on each capture.
  5. Let it run for the full shoot, then pull the SD card and copy the /timelapse folder to a computer.
  6. Install FFmpeg and run python3 assemble_timelapse.py --input ./timelapse --fps 24.
  7. Adjust INTERVAL_SEC and JPEG_QUALITY in the sketch to trade storage against shoot duration and image quality for the next run.

Troubleshooting

SymptomLikely CauseFix
"Camera init failed" on every bootPSRAM not enabled in board settingsTools → PSRAM → OPI PSRAM, then re-upload
"SD card mount failed"Card formatted exFAT, or a bad/loose cardReformat as FAT32; reseat the card
Upload fails / board not detectedGPIO 0 not grounded, or FTDI TX/RX swappedRecheck the programming jumper and wiring; try swapping TX/RX if nothing else works
Every frame looks slightly dark or color-shiftedThe "discard first frame" step got removed or skippedConfirm captureAndSave() still grabs and discards a warm-up frame before the real capture
Frame count lower than expected for the shoot durationBoard rebooted mid-shoot (power blip) or SD write failuresCheck Serial output during a shorter test run for "SD write failed" messages
Final video judders or skipsSome intervals longer than expected due to slow SD writesTry a faster (higher class) SD card, or increase INTERVAL_SEC slightly

Common Mistakes

Not enabling PSRAM before the first upload. This is responsible for most of the "camera works but the resolution is stuck at something tiny" reports, and it's a board-settings toggle, not a code fix.

Powering the board from a laptop's USB port for a long unattended shoot. It technically works for short tests, but a laptop USB port going to sleep, or the laptop itself closing, ends the shoot. A dedicated 5V wall supply is what actually survives an unattended overnight run.

Assuming the SD card and camera can operate independently. They share a bus. Trying to restructure this code to write frames asynchronously while capturing the next one will run into the same physical pin-sharing constraint this sketch is already built around — the sequential capture-then-write pattern here isn't a missed optimization, it's the actual hardware limit.

Skipping the discarded warm-up frame to "save time." It costs roughly 200 milliseconds per capture cycle, which is irrelevant at a 30-second interval, and removing it reintroduces the exposure-flicker artifact across the entire finished video.

Extensions to Try

  • Timestamp overlay — burn the capture time into each frame before saving, using a small bitmap font, so the finished timelapse shows elapsed time
  • Motion-triggered capture — skip a scheduled interval entirely and instead capture on PIR sensor trigger, useful for wildlife or driveway timelapses
  • WiFi upload instead of SD — stream frames to a home server over WiFi in projects where physically retrieving the SD card isn't convenient
  • Battery-and-solar operation — pair this with the same deep-sleep pattern covered in SolderHub's ESP32 Deep Sleep Guide for a fully untethered outdoor timelapse rig

Build Sheet

Take this build with you

We'll email you the parts list for ESP32-CAM Timelapse Recorder — Interval Photos to SD Card, with buy links and the Wokwi and source links, so you can order and build later. You'll also get new builds as they're published.

By subscribing you agree to our Privacy Policy. Unsubscribe anytime.

Q&A is currently disabled site-wide.

Related projects