<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Jupyter Blog - nbviewer</title><link href="https://jasongrout.github.io/medium-archive/pelican/" rel="alternate"/><link href="https://jasongrout.github.io/medium-archive/pelican/feeds/tag-nbviewer.atom.xml" rel="self"/><id>https://jasongrout.github.io/medium-archive/pelican/</id><updated>2020-03-06T19:46:00+00:00</updated><subtitle>The Project Jupyter blog: news, releases, and community stories, archived from blog.jupyter.org.</subtitle><entry><title>nbviewer has a new host: OVHcloud</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2020/nbviewer-has-a-new-host-ovhcloud/" rel="alternate"/><published>2020-03-06T19:46:00+00:00</published><updated>2020-03-06T19:46:00+00:00</updated><author><name>Min RK</name></author><id>tag:jasongrout.github.io,2020-03-06:/medium-archive/pelican/posts/2020/nbviewer-has-a-new-host-ovhcloud/</id><summary type="html">&lt;p&gt;nbviewer has moved from Rackspace to OVHcloud. That means moving from Docker to Kubernetes+Helm&lt;/p&gt;
</summary><content type="html">&lt;p&gt;For several years, &lt;a href="https://nbviewer.jupyter.org"&gt;nbviewer&lt;/a&gt; has been generously hosted by Rackspace. That sponsorship program appears to be ending, so nbviewer needed a new home; it has found one in &lt;a href="https://ovhcloud.com"&gt;OVHcloud&lt;/a&gt;. We are extremely grateful to OVHcloud for their support in keeping nbviewer running, building on their existing participation in the &lt;a href="/posts/2019/the-international-binder-federation/"&gt;Binder Federation&lt;/a&gt; (literally—nbviewer is now running on the same kubernetes cluster as ovh.mybinder.org).&lt;/p&gt;
&lt;h2 id="how-we-moved"&gt;How we moved&lt;/h2&gt;
&lt;h3 id="where-were-we-before"&gt;Where were we before?&lt;/h3&gt;
&lt;p&gt;nbviewer was previously deployed using a private repo (because it contained credentials) and various commands using &lt;a href="http://www.pyinvoke.org"&gt;invoke&lt;/a&gt;. It was a mixture of custom steps, using the openstack Python API and docker machine to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;allocate two VMs&lt;/li&gt;
&lt;li&gt;build docker images&lt;/li&gt;
&lt;li&gt;deploy two nbviewer instances per node&lt;/li&gt;
&lt;li&gt;deploy memcached via nbcache on each node&lt;/li&gt;
&lt;li&gt;deploy statuspage publisher as a separate step&lt;/li&gt;
&lt;li&gt;update fastly to point to the running nbviewer instances&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The upside was that we had a repo that could immediately deploy nbviewer to anywhere with docker. With this repo, we moved our deployment strategy from a CoreOS cluster &lt;a href="https://github.com/jupyter/nbviewer.org-deploy/commit/2bf42cdfd54552d2669549bda002a8bf5f3df60d"&gt;to&lt;/a&gt; Rackspace’s short-lived &lt;a href="https://www.rackspace.com/newsroom/carina-by-rackspace-simplifies-containers-with-easy-to-use-instant-on-native-container-environment"&gt;Carina service&lt;/a&gt; to &lt;a href="https://github.com/jupyter/nbviewer.org-deploy/commit/22f54fc971941764ec3e14a68174823397a56163"&gt;deploying VMs ourselves&lt;/a&gt; with docker-machine. Migrating to a new source of VMs would not have been hard, but it wouldn’t have solved any of our challenges.&lt;/p&gt;
&lt;p&gt;Known downsides of this deployment that we’ve experienced over the years:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;only a few folks ever knew how to use it and could thus deploy updates to nbviewer&lt;/li&gt;
&lt;li&gt;it was private, contributing to above. It’s hard to onboard folks in an open community to a private repo!&lt;/li&gt;
&lt;li&gt;independent machines meant cache was not shared (minor, but contributes to our consumption of the GitHub API rate limit)&lt;/li&gt;
&lt;li&gt;no automatic recovery based on health monitoring, so when a container had issues, some humans got automated emails but no action was automatically taken. Extra frustrating because the fix was ~always to restart the container.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The credentials used by this repo have been revoked and an archived version of the repo is &lt;a href="https://github.com/jupyter/nbviewer.org-deploy/tree/archive-rackspace"&gt;available&lt;/a&gt; (with credentials redacted from history) in the new, &lt;a href="https://github.com/jupyter/nbviewer.org-deploy"&gt;public nbviewer.org-deploy repo&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="what-have-we-learned"&gt;What have we learned?&lt;/h3&gt;
&lt;p&gt;We’ve learned a lot about open, automatic, and sustainable deployments, and can now comfortably address all of the downsides above. Most of this has been learned from the communities participating in the JupyterHub and Binder projects, as seen in the &lt;a href="https://github.com/jupyterhub/mybinder.org-deploy"&gt;mybinder.org-deploy&lt;/a&gt; repo. mybinder.org-deploy is a public repo that automatically deploys and tests updates to at least four different Kubernetes clusters at the push of a button (the Big Green Merge Button, to be precise). Some things we have learned in the years since we set up our nbviewer deployment:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;kubernetes and helm are great :)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/AGWA/git-crypt"&gt;git-crypt&lt;/a&gt; allows us to have public repos with some secret contents, so deployment repos don’t need to be fully private just to protect a couple api keys.&lt;/li&gt;
&lt;li&gt;Continuous Deployment via services like Travis or Circle lowers the bar to adding maintainers on a given deployment since all that’s needed is to press the Big Green Button.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The OVH sponsorship came in the form of a slice of a kubernetes cluster; that meant we had to migrate the nbviewer deployment tools from using docker-machine to kubernetes, which for us means helm.&lt;/p&gt;
&lt;h3 id="step-1-helm-chart-for-nbviewer"&gt;Step 1: helm chart for nbviewer&lt;/h3&gt;
&lt;p&gt;The first step was to create a helm chart for nbviewer, which is done &lt;a href="https://github.com/jupyter/nbviewer/pull/905"&gt;here&lt;/a&gt;. Before, nbviewer was two docker containers running nbviewer and one running memcached per server. To turn this into a helm chart we need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;dependency on &lt;a href="https://github.com/helm/charts/tree/master/stable/memcached"&gt;memcached helm chart&lt;/a&gt; for easy deployment of the cache. All the nbviewer instances will talk to this memcache, which should improve our GitHub rate limit consumption, since the cache will be shared. This means the deprecation of our own &lt;a href="https://github.com/jupyter/nbcache"&gt;nbcache repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Deployment&lt;/code&gt; for nbviewer. This is (one of) the kubernetes wrappers around containers. It has a nice ‘replicas’ field to easily scale nbviewer up and down. We are currently running with 3 replicas.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Service&lt;/code&gt; to expose nbviewer to the Internet (likely also need an ingress in the future)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Deployment&lt;/code&gt; for the statuspage publisher, which updates &lt;a href="https://status.jupyter.org"&gt;https://status.jupyter.org&lt;/a&gt; with the remaining github rate limit available&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We also needed helm charts and docker images for some other components, such as cdn.jupyter.org and the nbviewer statuspage data source.&lt;/p&gt;
&lt;h3 id="step-1b-helm-chart-for-cdnjupyterorg"&gt;Step 1b: helm chart for cdn.jupyter.org&lt;/h3&gt;
&lt;p&gt;nbviewer and some other services interact with cdn.jupyter.org, which is a lightweight nginx configuration to serve the classic notebook’s javascript and css as static files(this is no longer needed for npm-based jupyterlab, which can use &lt;a href="https://unpkg.com"&gt;unpkg&lt;/a&gt; as a CDN). This used to run on a multipurpose server VM, but that is also being retired for the same reason. The result was creating a docker image and helm chart for serving the contents of cdn.jupyter.org. The scripts used to run the existing CDN were already in &lt;a href="http://github.com/jupyter/cdn.jupyter.org/"&gt;a repo&lt;/a&gt;, so it was a small amount of work to adapt this to run in a container instead of on a server. We may retire cdn.jupyter.org in the future, so please don’t rely on it :)&lt;/p&gt;
&lt;p&gt;This is where we are right now — nbviewer.jupyter.org and cdn.jupyter.org are being served by OVH and the Rackspace machines are being retired.&lt;/p&gt;
&lt;h3 id="step-2-automatic-deployment"&gt;Step 2: automatic deployment&lt;/h3&gt;
&lt;p&gt;It would have been ideal for this to be step 1, but that’s now how it happened. Sometimes you need to get it done quickly before you get it done right. The task here will be to make a new, public nbviewer-deploy repo following the patterns we have learned in mybinder.org-deploy. That will mean:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;create public nbviewer.org-deploy repo with configuration and git-crypt encrypted secrets for deploying nbviewer on the ovh cluster (&lt;a href="https://github.com/jupyter/nbviewer.org-deploy"&gt;done&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;adopting &lt;a href="https://github.com/jupyterhub/chartpress"&gt;chartpress&lt;/a&gt; to publish our helm charts for nbviewer and version-tagged images from the nbviewer repo&lt;/li&gt;
&lt;li&gt;configure Travis-CI or other CI service to automatically deploy updates with helm, so merging a PR is all we need to do to deploy updates to nbviewer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thanks to OVHCloud for their continued support of Jupyter and Binder, and the JupyterHub and Binder communities for teaching me how to operate services in the open with kubernetes and helm.&lt;/p&gt;
</content><category term="Kubernetes"/><category term="nbviewer"/></entry><entry><title>nbviewer and GitHub’s rate limit</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/" rel="alternate"/><published>2017-03-01T07:45:00+00:00</published><updated>2017-09-25T09:25:00+00:00</updated><author><name>Project Jupyter</name></author><id>tag:jasongrout.github.io,2017-03-01:/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/</id><summary type="html">&lt;p&gt;If you’ve been using nbviewer recently, you may have seen this message more often than you would like: It turns out that some overzealous SEO bots were eating up …&lt;/p&gt;
</summary><content type="html">&lt;p&gt;If you’ve been using &lt;a href="https://nbviewer.jupyter.org"&gt;nbviewer&lt;/a&gt; recently, you may have seen this message more often than you would like:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/g001-Screen-Shot-2017-02-26-at-21.31.09.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;It turns out that some overzealous SEO bots were eating up all of our resources. Last week, we discovered this cause of the API rate limit exhaustion and took a few actions to resolve it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;tl;dr;&lt;/strong&gt; nbviewer shouldn’t hit the rate limit so often anymore. This is what our API usage availability has looked like since we deployed the changes I’ll describe here:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/g002-rate-limit-copy.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;h3 id="nbviewers-architecture"&gt;nbviewer’s architecture&lt;/h3&gt;
&lt;p&gt;I’d like to take this opportunity to give an overview of how we deploy nbviewer, so we can see how each piece comes into play.&lt;/p&gt;
&lt;p&gt;To start off, nbviewer is two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the &lt;a href="https://github.com/jupyter/nbviewer"&gt;web application&lt;/a&gt; that finds notebooks on the internet and renders them, and&lt;/li&gt;
&lt;li&gt;the specific deployment of nbviewer serving &lt;a href="https://nbviewer.jupyter.org"&gt;https://nbviewer.jupyter.org&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You can even roll your own nbviewer by grabbing the public docker image and running:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;docker pull jupyter/nbviewer
docker run -p 5000:5000 -it jupyter/nbviewer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can then start rendering notebooks left and right. However, there’s a limit to how scalable such a basic deployment can be.&lt;/p&gt;
&lt;h3 id="rendering-a-notebook"&gt;Rendering a notebook&lt;/h3&gt;
&lt;p&gt;Rendering a notebook involves two major steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;download the notebook from somewhere on the web&lt;/li&gt;
&lt;li&gt;render it to HTML with &lt;a href="https://github.com/jupyter/nbconvert"&gt;nbconvert&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each step can take a bit of time. Fetching and rendering a notebook start to finish can take as much few seconds if the notebook is big and/or the upstream server is busy or slow. We use tornado’s async APIs to handle concurrent requests. Fetching notebooks from the web is handled via a subclass of &lt;a href="http://www.tornadoweb.org/en/stable/httpclient.html#tornado.httpclient.AsyncHTTPClient"&gt;AsyncHTTPClient&lt;/a&gt; and rendering is done in the background via &lt;code&gt;concurrent.futures&lt;/code&gt;. A single nbviewer instance can handle quite a few concurrent visitors, depending on how many rendering threads are available.&lt;/p&gt;
&lt;h3 id="caching"&gt;Caching&lt;/h3&gt;
&lt;p&gt;Most notebooks on the web don’t change very often, so going through the whole process when you just downloaded and rendered the same notebook a minute ago is a bit of a waste. &lt;strong&gt;This is where caching comes in.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;nbviewer supports caching via a simple in-memory cache or &lt;a href="https://memcached.org"&gt;memcache&lt;/a&gt; at two levels:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every time nbviewer completes a render, it caches the result for a period of time (usually 10 minutes to an hour, depending on render time), so that a very popular notebook doesn’t consume too many resources. This is unconditional, and doesn’t take into account whether the original notebook has been updated. If it’s in the cache, it gets reused.&lt;/li&gt;
&lt;li&gt;nbviewer &lt;em&gt;separately&lt;/em&gt; caches upstream requests. When nbviewer asks an external service for anything, whether it’s a regular web server or GitHub’s API, the response is saved for a time. The next time the same resource is requested, nbviewer checks for a cached response. Unlike the full render cache, this cache doesn’t prevent a new upstream request. Instead, we check the headers of the cached reply (&lt;code&gt;ETag&lt;/code&gt; and &lt;code&gt;Last-Modified&lt;/code&gt;). nbviewer then takes the content of those headers to populate &lt;code&gt;If-None-Match&lt;/code&gt; and &lt;code&gt;If-Modified-Since&lt;/code&gt; headers before sending a new request. The server can then check these headers and return an empty reply with status 304, telling nbviewer that it should reuse the cached response instead of running the full process of handling the request again. Critically for nbviewer, when the GitHub API responds with 304, the rate limit &lt;a href="https://developer.github.com/v3/#conditional-requests"&gt;is not consumed&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="our-nbviewer-deployment"&gt;Our nbviewer deployment&lt;/h3&gt;
&lt;p&gt;To deal with the traffic our public nbviewer instance gets, we have deployed nbviewer with a few extra elements. The moving parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;we have multiple nbviewer instances on multiple servers deployed. Right now, there are two nbviewer instances (running in docker) on each of two servers running on &lt;a href="https://rackspace.com"&gt;rackspace&lt;/a&gt;, making four total instances.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://memcached.org"&gt;memcache&lt;/a&gt;, also running in docker, with separate instances on each server&lt;/li&gt;
&lt;li&gt;&lt;a href="https://fastly.com"&gt;fastly&lt;/a&gt; sits in front of our nbviewer instances and load-balances requests across them, performing health checks, etc. It also has its own cache layer.&lt;/li&gt;
&lt;li&gt;Finally, &lt;a href="https://cloudflare.com"&gt;cloudflare&lt;/a&gt; is out front, handles SSL termination, and a small amount of caching&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Four instances is quite a bit more than we need for the load nbviewer gets, but having it spread out lets us do things like reboot or upgrade servers without any downtime.&lt;/p&gt;
&lt;h3 id="what-happened"&gt;What happened?&lt;/h3&gt;
&lt;p&gt;So, why were we seeing the GitHub rate limit hit so often? Did nbviewer just get too popular? Sadly, no. How did we figure out what happened? &lt;em&gt;To the logs!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fun fact: until now, we were using docker’s default json logging, and nbviewer was producing 50GB of logs each month. &lt;code&gt;docker logs --tail&lt;/code&gt; does not behave super well with a 30GB log file. I started digging through the logs to see what was going on, but quickly realized that this wasn’t working very well. To try to get a better sense of things, I started by hooking up our logs to &lt;a href="https://loggly.com"&gt;Loggly&lt;/a&gt; via syslog. Loggly lets me start to extract some metrics, such as status, URL, IP address, and user-agent of failing requests.&lt;/p&gt;
&lt;h3 id="what-did-we-learn"&gt;What did we learn?&lt;/h3&gt;
&lt;p&gt;One of the first things I did was group incoming requests by IP address, to see if anything stood out. This is what I saw:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/g003-Screen-Shot-2017-02-24-at-19.30.41.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;So about two thirds of requests were coming from just two ip addresses! That’s not right. Checking for user-agent showed something else:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/g004-Screen-Shot-2017-02-24-at-19.30.14.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;Two bots, &lt;a href="http://ahrefs.com/robot/"&gt;Ahrefs&lt;/a&gt; and &lt;a href="https://moz.com/researchtools/ose/dotbot"&gt;DotBot&lt;/a&gt; were accounting for almost three quarters of requests. That’s too much.&lt;/p&gt;
&lt;h3 id="what-we-did-about-it"&gt;What we did about it&lt;/h3&gt;
&lt;p&gt;The very first thing we did was to block the two IP addresses that were driving way too much traffic. This was done in the fastly layer, sending a custom &lt;a href="https://httpstatuses.com/429"&gt;429 error&lt;/a&gt; for any request from these IPs:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/001-1_lHjj_gdpvFSHMQD1Y0GETQ.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;This improved availability a bunch, and would probably have been plenty on its own to resolve the current situation. Of course, it wouldn’t be enough to prevent similar situations in the future. Since the problem was caused by robots, the next place to look was &lt;a href="http://www.robotstxt.org"&gt;robots.txt&lt;/a&gt;. Our robots.txt had been the most permissive possible:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;User-agent: *
Disallow:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;explicitly allowing any bot. Turns out we were being too nice. We’ve now updated our robots.txt to tell all robots to slow way down, and instructed the two overzealous bots that they are not welcome:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;User-agent: *
Crawl-delay: 10
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;User-agent: dotbot
Disallow: /
User-agent: AhrefsBot
Disallow: /
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Again, this should be enough to resolve the issue &lt;strong&gt;if these and all other bots are honorable&lt;/strong&gt; and respect the robots.txt.&lt;/p&gt;
&lt;p&gt;However, we would prefer to not rely on the kindness of strange robots.&lt;/p&gt;
&lt;h3 id="better-caching-and-better-use-of-the-cache"&gt;Better caching and better use of the cache&lt;/h3&gt;
&lt;p&gt;As mentioned earlier, we cache the responses we get from upstream sources and use those to reduce the load on the GitHub API and other sources of notebooks. However, we were not making the best use of that cache. First of all, the responses were only being cached for two hours, which means this caching only help us for relatively short bursts of activity (e.g. a popular notebook being shared on social media). This certainly happens, but does not represent a large fraction of typical traffic. Since the way we check for updates with &lt;code&gt;If-Not-Modified&lt;/code&gt; has no risk of stale results, we &lt;a href="https://github.com/jupyter/nbviewer/pull/674/files"&gt;changed this&lt;/a&gt; to cache upstream responses forever, until memcache pushes them out when it gets full. Since most public notebooks don’t change often, we should get a cache hit almost every time.&lt;/p&gt;
&lt;p&gt;The next change we made was an improvement to behavior when the rate limit is hit. Previously, if we had a cached upstream response and GitHub told us there was an update that we couldn’t fetch due to the rate limit, we would serve the 503 error page. But we had valid data right there! Now, if we hit the rate limit but happen to have an old version of the data, we &lt;a href="https://github.com/jupyter/nbviewer/pull/674/files#diff-a9fe8780e6ab9856bafdf9c997a70c56R82"&gt;work with the stale version&lt;/a&gt; rather than serving an error page. Something’s better than nothing, right? This applies to any kind of error fetching from an upstream server, not just failures due to hitting the GitHub rate limit.&lt;/p&gt;
&lt;p&gt;So we are now much less likely to hit the rate limit in the first place, and much more likely to be able to serve &lt;em&gt;something&lt;/em&gt; if and when we hit it.&lt;/p&gt;
&lt;h3 id="our-own-rate-limit"&gt;Our own rate limit&lt;/h3&gt;
&lt;p&gt;The last thing we did to help limit the consumption of resources by a small number of actors was to &lt;a href="https://github.com/jupyter/nbviewer/pull/675"&gt;implement our own rate limit&lt;/a&gt; in nbviewer. This is a pretty simple rate limit, where we identify a source by the combination of ip address and user-agent. Currently, this limit is set at 60 requests in ten minutes. We only count non-cached requests against the limit, so refreshing the same few pages a bunch won’t consume the limit.&lt;/p&gt;
&lt;p&gt;Like the cache, the rate limit is actually implemented with memcache. When we see a request from a given source, we initialize a counter to memcache with an expiry of our rate limit window:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;key = cache_key(request)  # combines ip and user-agent
new_visitor = memcache.add(key, 1, rate_limit_window)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If it’s not a new visitor, we increment the counter with memcache’s atomic increment operation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if not new_visitor:
    count = memcache.incr(key)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The last thing to do is to check if the rate limit is exceeded, and return an HTTP 429 error instead of proceeding with the render:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if count &amp;gt; rate_limit:
    raise HTTPError(429, &amp;quot;Rate limit exceeded...&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you hit the rate limit you will see:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2017/nbviewer-and-githubs-rate-limit/images/g006-Screen-Shot-2017-02-27-at-12.01.08.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;From looking at our logs, we can see that this rate limit has blocked ~800 requests since deploying it a few days ago. More than 600 of these were from &lt;a href="https://domainreanimator.com"&gt;a bot&lt;/a&gt; that apparently doesn’t respect robots.txt. A few dozen were made from phantomjs, presumably some manual scraping that made a few too many fetches. That leaves a few probably-real humans who hit the rate limit, but no IP has hit it often or for a sustained period of time.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jupyter/nbviewer/issues"&gt;Let us know&lt;/a&gt; if the rate limiting is causing an issue for you. We will keep an eye on the limits, and continue to tune both the limits and limiting conditions to find the best balance.&lt;/p&gt;
&lt;h3 id="monitoring"&gt;Monitoring&lt;/h3&gt;
&lt;p&gt;We run our instances with the &lt;a href="https://docs.newrelic.com/docs/agents/python-agent/getting-started/introduction-new-relic-python"&gt;New Relic&lt;/a&gt; Python agent, which gets us some basic health monitoring. We also have some monitoring and alerts in our Rackspace account to notify us when things go down.&lt;/p&gt;
&lt;p&gt;We’ve added the error rate tracked by New Relic and a &lt;a href="https://gist.github.com/179f92a45e18e865ac4b95578dce949c"&gt;new metric&lt;/a&gt; that tracks how much of our GitHub API limit is available to our &lt;a href="http://status.jupyter.org"&gt;public status page&lt;/a&gt;, so you can see how healthy nbviewer is at any given time.&lt;/p&gt;
&lt;h3 id="one-last-thing"&gt;One last thing&lt;/h3&gt;
&lt;p&gt;After confirming that everything was working well, I did a final deploy on a Friday evening, and moved on. Then this came in:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://twitter.com/williamrthomas/status/835255588116447232"&gt;https://twitter.com/williamrthomas/status/835255588116447232&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Turns out&lt;/em&gt; there had been an update to the New Relic Python agent since the last deploy, which introduced this tiny off-by-one error:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;if len(args) &amp;gt; 0:
    args = list(args)
    args[1] = wrapped_callback
    # what if len(args) == 1 ?!
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(that &lt;code&gt;&amp;gt; 0&lt;/code&gt; should be &lt;code&gt;&amp;gt; 1&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;This little bug just so happens to prevent nbviewer from making any outgoing requests :shrug:. This served as a friendly reminder that it is a good idea to pin your dependencies when deploying applications so that updates only happen when you decide they should.&lt;/p&gt;
</content><category term="nbviewer"/></entry><entry><title>Rendering Notebooks on GitHub</title><link href="https://jasongrout.github.io/medium-archive/pelican/posts/2015/rendering-notebooks-on-github/" rel="alternate"/><published>2015-05-07T19:47:00+00:00</published><updated>2017-08-28T20:08:00+00:00</updated><author><name>Project Jupyter</name></author><id>tag:jasongrout.github.io,2015-05-07:/medium-archive/pelican/posts/2015/rendering-notebooks-on-github/</id><summary type="html">&lt;p&gt;We are pleased to announce that, starting today, and as announced on the GitHub blog, Jupyter/IPython notebook (.ipynb) files will render directly on GitHub. This …&lt;/p&gt;
</summary><content type="html">&lt;p&gt;&lt;img src="https://jasongrout.github.io/medium-archive/pelican/posts/2015/rendering-notebooks-on-github/images/001-1_Edn_LpbSpLeNKfWkEdG2Jg.webp" alt="" loading="lazy" data-body-image=""&gt;&lt;/p&gt;
&lt;p&gt;We are pleased to announce that, starting today, and as announced on &lt;a href="https://github.com/blog/1995-github-jupyter-notebooks-3"&gt;the GitHub blog&lt;/a&gt;, Jupyter/IPython notebook (&lt;code&gt;.ipynb&lt;/code&gt;) files will render directly on GitHub. This feature works for notebooks in any of the supported Jupyter &lt;a href="https://github.com/ipython/ipython/wiki/IPython-kernels-for-other-languages"&gt;programming languages&lt;/a&gt; on both public and private repos. This capability will complement &lt;a href="https://nbviewer.jupyter.org"&gt;nbviewer&lt;/a&gt; and make it easier for GitHub users to create, view and share notebooks on GitHub.&lt;/p&gt;
&lt;p&gt;Here are some great examples of notebook based content that can now be viewed directly on GitHub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cameron Davidson-Pilon’s &lt;a href="https://github.com/CamDavidsonPilon/Probabilistic-Programming-and-Bayesian-Methods-for-Hackers/blob/master/Chapter1_Introduction/Chapter1.ipynb"&gt;Probabilistic Programming and Bayesian Methods for Hackers&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Lorena Barba’s &lt;a href="https://github.com/barbagroup/AeroPython/blob/master/lessons/01_Lesson01_sourceSink.ipynb"&gt;Aerodynamics-Hydrodynamics with Python&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Benjamin Laken’s &lt;a href="https://github.com/benlaken/Comment_BadruddinAslam2014/blob/master/Monsoon_analysis.ipynb"&gt;Monsoon Analysis&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We want to thank the GitHub team for bringing this new feature to GitHub. In particular &lt;a href="https://github.com/tclem"&gt;@tclem&lt;/a&gt;, &lt;a href="https://github.com/arfon"&gt;@arfon&lt;/a&gt;, &lt;a href="https://github.com/sshirokov"&gt;@sshirokov&lt;/a&gt;, and &lt;a href="https://github.com/skalnik"&gt;@skalnik&lt;/a&gt; have all done an incredible job building this at GitHub. The first discussion of how to bring notebook rendering to GitHub happened almost 18 months ago, and a lot of work made this feature possible today. With over &lt;a href="https://github.com/search?l=&amp;amp;q=nbformat+extension%3Aipynb&amp;amp;ref=advsearch&amp;amp;type=Code&amp;amp;utf8=%E2%9C%93"&gt;200,000 notebooks&lt;/a&gt; available on GitHub, you already have a lot to read!&lt;/p&gt;
&lt;p&gt;We are excited to see how this feature will impact science education and research on GitHub, as well as encourage the adoption of the Jupyter notebook as a standard file format across a wide range of fields. Jupyter notebook files are a JSON-based open document format that supports code and results, narrative text, images, and equations in one file. These documents simplify the creation and communication of computational narratives and make the sharing and replication of scientific results and data analyses simpler.&lt;/p&gt;
&lt;h3 id="nbviewer"&gt;nbviewer&lt;/h3&gt;
&lt;p&gt;One key piece of infrastructure in sharing notebooks has been the &lt;a href="http://nbviewer.org"&gt;nbviewer&lt;/a&gt; service, which started as a side project of mine in the late summer of 2012. Nbviewer has grown through deep integration with GitHub, with the ability to browse all public users, organizations, repositories, branches, tags, and even Gists. This would not have been possible without &lt;a href="https://github.com/rgbkrk"&gt;@rgbkrk&lt;/a&gt; and &lt;a href="https://github.com/bollwyvl"&gt;@bollwyvl&lt;/a&gt;, who handle the development and deployment of nbviewer. It would also not have been possible without &lt;a href="https://developer.rackspace.com/"&gt;Rackspace&lt;/a&gt; and &lt;a href="http://www.fastly.com/"&gt;Fastly&lt;/a&gt;, who provide free hosting and fast static asset distribution. Nbviewer has steadily grown to render hundreds of thousands of notebooks every week, and usage is still increasing. Based on this, we believe that rendering on GitHub will be a highly used and appreciated feature.&lt;/p&gt;
&lt;p&gt;Although you can now view notebooks directly on GitHub, we are not planning to stop the development of nbviewer. While rendering directly in GitHub repositories is convenient and allows viewing notebooks in private repositories, there are number of reasons that nbviewer will continue to remain an important part of project Jupyter.&lt;/p&gt;
&lt;p&gt;First, there are many notebooks not hosted on GitHub. As before, these notebooks can still be viewed on nbviewer. Furthermore, the Jupyter/IPython team are actively working on improving integration with other content providers, building on our experience with GitHub. For example, a recent pull request of &lt;a href="https://github.com/jupyter/nbviewer/pull/443"&gt;@bollwyvl&lt;/a&gt; streamlines the process of adding new providers to nbviewer, with &lt;a href="https://github.com/jupyter/nbviewer/issues?q=is%3Aopen+is%3Aissue+label%3Aprovider"&gt;proposals&lt;/a&gt; for DropBox, GitLab, Google Drive, Stash, and other hosting and collaboration platforms.&lt;/p&gt;
&lt;p&gt;Second, because of security concerns, some features available on nbviewer will not be available on GitHub. For example, GitHub will not render any dynamic output display that uses JavaScript, custom CSS, and most custom HTML embedded in Markdown or in outputs. When this is the case, and GitHub cannot fully render your notebooks, you will see an icon that allows you to view the full notebook on nbviewer.&lt;/p&gt;
&lt;p&gt;Because nbviewer does not handle authentication, it will continue to support custom CSS, HTML and JavaScript in notebook. Thus, for highly customized or dynamic notebooks, nbviewer will still be the ideal place to share them.&lt;/p&gt;
&lt;p&gt;We would love your feedback! If you have comments or find bugs with notebook rendering on GitHub or nbviewer, please open an issue on our &lt;a href="https://github.com/jupyter/nbviewer"&gt;nbviewer repo&lt;/a&gt;. Also, please send a few tweets to thank all the people without whom this would have not happened.&lt;/p&gt;
&lt;p&gt;The Jupyter/IPython Team&lt;/p&gt;
</content><category term="GitHub"/><category term="nbviewer"/><category term="publishing"/></entry></feed>