Getting Historical Candlestick Data From Binance
Most people hitting this for the first time expect to download a CSV and be done with it. The reality is messier. Binance doesn't offer a simple bulk export button for historical klines. You work with the REST endpoint directly, which means writing a small script or using an existing library. The endpoint is /api/v3/klines and it returns OHLCV data for up to 1000 candles per request.The first thing you need to understand is that Binance's rate limits are not generous for bulk historical retrieval. The public endpoint has a weight limit of 600 requests per minute for IP-based throttling. If you're pulling five years of daily candles for a single pair, that's roughly 1,825 requests minimum. At the default limit, you're looking at over three minutes of pure wait time just for one symbol, and that doesn't count retries when the endpoint starts returning 429 errors. The actual workflow looks like this. You fetch the oldest chunk first using the startTime and endTime parameters. Binance returns the most recent 1000 candles within your window. Then you take the timestamp of the oldest candle from that response and subtract one millisecond to use as the next request's endTime. You repeat this until you have everything you need. The pagination is entirely cursor-based on timestamps, not offsets. This is important because it means you cannot jump ahead arbitrarily. You must walk through the data sequentially. I ran into a specific issue last year when I was building a backtest pipeline for BTC/USDT daily candles going back to 2017. Binance's API has a hard limit on the total time range you can query in a single request. If you set a startTime too far back relative to your endTime, the endpoint simply returns an empty array instead of an error. I wasted about two hours thinking my code was broken before I realized the 90-day window restriction applies differently depending on the interval. For 1d candles, you can actually query further back, but I kept hitting silent failures because I was miscalculating the window boundaries. The workaround was to always validate the length of the returned array before proceeding, and when it came back empty, trim your startTime forward by increments until you got data again.
For anyone who wants to move faster than raw REST requests allow, the websocket kline stream is an option but it only gives you real-time data, so it doesn't help with historical retrieval. Your main alternative is the Binance Data Center, which used to offer some free historical datasets in JSON and CSV formats. They've been inconsistent about availability though, and the data quality there is not guaranteed to match what the live API returns. I would treat any downloaded dataset as a supplement, not a replacement for pulling directly from the API.
Practical Implementation Details
Here is what a functional Python approach looks like without any external dependencies beyond the standard requests library. You define your symbol, interval, and date range. Then you loop through requesting chunks and accumulate them. Key parameter combinations: symbol - Trading pair like BTCUSDT. Always uppercase with no separator.
Get the Full Details

interval - Supported values include 1m, 5m, 15m, 1h, 4h, 1d, 1w. Some less common intervals like 30s and 3m exist but are not well documented and may return empty results for certain symbols. startTime/endTime - Unix millisecond timestamps. Python's datetime module converts easily with int(ts * 1000). One thing nobody warns beginners about is that Binance silently drops candles for certain obscure trading pairs during periods of low liquidity or exchange maintenance. If you're aggregating data across multiple altcoins, you will find gaps that don't correspond to any known exchange outage. My approach was to maintain a gap report that flags any interval where the time delta between consecutive candles exceeds the expected interval by more than 50 percent. That catches most of the missing data points.
Common Pitfalls That Wreck Projects
The most frequent mistake is assuming that candle close times align perfectly with wall-clock boundaries. For 1h candles, a close time of 1700000000000 does not necessarily mean the candle represents 00:00 to 00:59 UTC. Binance aligns its candles to the exchange server's internal clock, and there can be slight drift. For most trading strategies this drift is negligible, but if you're syncing Binance data with another exchange's data or with on-chain events, misaligned timestamps will cause synchronization errors that are extremely difficult to debug later. Another issue is that the open time and close time fields in the response are not always consistent with the actual candle boundaries for illiquid markets. During periods of zero trading activity, Binance may still emit a candle with a zero volume and a close price identical to the previous candle. If your strategy treats zero-volume candles as valid signals, you are introducing subtle bugs. I filter these out by checking that the volume field is greater than zero before processing any candle. The API also changes behavior occasionally without announcement. In late 2024, Binance adjusted how some lower-tier altcoin pairs handle their kline data during volatile periods. Candles that should have been emitted as separate intervals got merged or dropped entirely. There is no official documentation about these edge cases. The only way to catch them is to cross-reference your cached data against periodic spot checks using the API in real time.
Rate Limit Management That Actually Works
Instead of blindly hammering the endpoint and getting throttled, implement a token bucket rate limiter. Binance's public endpoints use a weight system where each kline request costs one weight unit. You can track your remaining quota by reading the X-MBX-USED-WEIGHT header from each response. When you hit 80 percent of your limit, throttle down or sleep for a few seconds. I usually set a target of 50 requests per minute for long-running historical fetches, which keeps me comfortably under the limit while still completing multi-year datasets in under ten minutes per symbol on a decent connection. If you need to pull data for dozens of symbols regularly, consider creating a Binance account and using API keys with a higher weight limit. The authenticated endpoints give you significantly more headroom. Even the basic account tier boosts your limit well above the anonymous 600-weight per minute cap. This matters if you are doing daily data refreshes and need to re-fetch everything without waiting around.

When The API Approach Fails Completely
There are scenarios where pulling from Binance directly is not viable. If you need tick-level or trade-level historical data going back years, the REST API will not serve you. Binance's public trade endpoint only retains a limited window of data, and it is not designed for archival retrieval. For that level of granularity you are either paying for a third-party data provider or running your own data ingestion pipeline that has been recording trades since before you needed the data. Data accuracy is another limitation. Binance has been criticized in the past for price manipulation on thin markets, particularly on leverage token pairs and newly listed coins. If your strategy depends on price data from those instruments, the historical records may not reflect what actually happened on other venues. I always cross-reference suspicious price movements against CoinGecko or CoinMarketCap aggregated prices before making any trading decisions based purely on Binance's data. The API has no built-in data validation or completeness guarantee. It will give you exactly what it has, gaps and all. You are responsible for noticing when something is missing. The burden of data quality checking sits entirely on whoever writes the extraction script. Factor that into your timeline. What looks like a two-hour data gathering task often turns into a full day once you account for validation, gap detection, and timestamp reconciliation.