On looping
while...end
If you need to break out of the loop, while condition;code;end will probably be shorter than loop{code;condition||break}.
The ; before end is not always required, eg. while condition;p("text")end
until c;...;end is equivalent to while !c;...;end and 1 byte shorter.
Note that in most cases code while condition and code until condition are significantly shorter as they don't require the end keyword and can often drop semicolons. Also, i+=1 while true is equivalent to i+=1while true and 1 byte shorter.
redo
When run, the redo command jumps back to the beginning of the block it's in.
When using redo in a lambda, you will have to move any setup variables to the arguments to avoid them being reset at every iteration (see examples).
recursion
Recursion can be shorter is some cases. For instance, if you're working on an array element by element, something like f=->s,*t{p s;t[0]&&f[*t]} can be shorter than the alternatives depending on the stuff.
Note that per the current consensus, if you're calling your function by name, you need to include the assignment (f=) in the byte count making all recursive lambdas 2 bytes longer by default.
eval
If you need to run some code n times, you can use eval"code;"*n.
This will concatenate code; n times and run the whole thing.
Note that in most cases you need to include a ; after your code.
Examples
A lambda to print all numbers from 1 to a inclusive:
->n{i=0;loop{p i+=1;i<n||break}} # 32 bytes f=->n,i=1{i>n||p(i)&&f[n,i+1]} # 30 bytes ->n,i=0{p(i+=1)<n&&redo} # 24 bytes ->n{i=0;p i+=1while i<n} # 24 bytes ->n{i=0;eval"p i+=1;"*n} # 24 bytes ->n{n.times{|i|p i+1}} # 22 bytes # thanks to @benj2240 ->n{n.times{p _1+1}} # 20 bytes # thanks to @AgentIvan In this case, since the end-point is defined (n), the n.times loop is the shortest.
The redo loop works because i+=1 modifies i and returns its new value and p(x) returns x (this is not true of print and puts).
Given a function g and a number n, find the first number strictly larger than n for which g[n] is truthy
->g,n{loop{g[n+=1]&&break};n} # 29 bytes f=->g,n{g[n+=1]?n:f[g,n]} # 25 bytes ->g,n{1until g[n+=1];n} # 23 bytes ->g,n{(n+1..).find &g} # 22 bytes ->g,n{g[n+=1]?n:redo} # 21 bytes In this case, with an unknown end-point, redo is the best option.
The (n+1..Inf) loop is equivalent to simply looping indefinitely but more verbose.
A 1 (or anything else) is required before the until keyword to complete the syntax, using a number allows you to drop a space.
The eval method is not viable in this case because there is neither a defined end-point nor an upper bound.
Update: with the new open ranges (n+1..Inf) can be written simply as (n+1..), also .find{|x|g[x]} is equivalent to .find &g where g is converted to a block.
TL;DR check out redo, it can very often shave off a couple of bytes